diff --git a/NEWS.md b/NEWS.md index 3221d53..1a8988b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,81 @@ # 📰 Picopt News +## v6.6.0 + +### Fixes + +- Files are now replaced atomically: a crash, kill, or full disk mid-write can + no longer destroy the original image. +- Archive members whose optimization fails are kept in the repacked archive; + previously they were silently dropped. +- Nested archives (e.g. a CBZ inside a ZIP) now optimize; previously they always + errored. +- Archives keep their comments and member order through repack; tar symlinks and + hardlinks survive; 7z member times are preserved; pre-1980 member times no + longer crash conversion; legacy non-UTF8 zip member names are repaired. +- Archive conversions compress each member by content; converting a rar, tar, or + 7z no longer leaves every member stored uncompressed. +- Files that fail to optimize are retried on the next timestamped run; + previously directory timestamps covered them and they were skipped forever. +- One corrupt file inside an archive (e.g. a decompression bomb) no longer + aborts the entire run. +- Boolean options set in config files or environment variables are no longer + ignored. +- Requested archive conversions (e.g. CBR to CBZ) now happen even when the + contents are already optimized. +- Lossless WebPs with large metadata inside archives are no longer misdetected + as lossy. +- Owner-password-restricted PDFs keep their encryption; optimized PDF images + keep their color transform; more signed PDFs are detected and refused. +- svgo no longer strips viewBox, and keeps title/desc metadata unless + `--strip-metadata` is given. +- Conversions discarded for being bigger (and dry runs) are reported as skips, + not errors. +- `--preserve` no longer fails as a non-root user when it cannot change + ownership; it still restores permissions and modification time. +- Temporary files and animated-WebP frame directories are cleaned up when a tool + fails or a run is cancelled, instead of leaking to disk. +- `picopt doctor` no longer reports missing tools and exits nonzero on healthy + installs. +- Symlink loops, unreadable directories, and FIFOs no longer hang or crash the + walk. +- Bad `--memory-limit` and `--after` values abort with clean error messages; + timezone-aware `--after` values are honored; lowercase format lists are + accepted for `-x` and `-c`. +- `-I`/`--no-default-ignores` without `-i` no longer aborts at startup. +- Existing timestamps are invalidated once after upgrading because the timestamp + config check gained new keys; the first `-t` run re-examines already-optimized + trees. + +### Features + +- Per-directory `.picopt.yaml` config files: pin any setting to a directory + tree. Deeper directories win; environment variables and the command line still + override. Editing one re-processes its tree on timestamped runs. +- Write your invoked options to config files: `-w` to the user config, `-W` to a + `.picopt.yaml` in each target directory, `--write-config-file PATH` to an + explicit path. +- Memory-aware scheduling: picopt now estimates the peak memory of large + archives and limits how many run at once so big libraries (e.g. multi-GB comic + archives) no longer exhaust RAM and get the process OOM-killed. Tune the + budget with the new `--memory-limit` option (e.g. `--memory-limit 8G`), which + reads as an approximate peak-memory target; the default is two-thirds of + detected RAM. +- Incremental archive re-optimization: when a timestamped archive changes, + members older than its timestamp are skipped instead of re-optimized; `-E` + disables the member check. Timestamp files found inside archives are consumed + and removed on repack. + +### Performance + +- Archive member format detection runs in parallel workers instead of + serializing the scheduler. +- Solid 7z archives extract in one pass instead of decompressing from the start + for every member. +- The progress pre-scan is skipped in quiet mode, less image data is shipped + between processes, and scheduling under a memory limit no longer rescans the + whole queue on every completion. + ## v6.5.2 - Update to latest oxipng v10 diff --git a/README.md b/README.md index f4f5d93..7086b52 100644 --- a/README.md +++ b/README.md @@ -110,15 +110,9 @@ When configured to convert GIFS to WebP, Animated GIFs are converted to WebP with the [gif2webp](https://developers.google.com/speed/webp/docs/gif2webp) binary if it exists. It is normally distributed as part of the webp package. -#### Experimental option - -This experimental options is activated with an environment variable. - -Set `PICOPT_ENABLE_IMG2WEBP=1` to make picopt optimize animated WebPs and -convert animated PNGs to webp with the -[img2webp](https://developers.google.com/speed/webp/docs/img2webp) binary. In my -experiments img2webp has performed worse than picopt's custom algorithm using -PIL & cwebp +Animated WebPs are optimized (and animated PNGs converted to WebP) with the +[img2webp](https://developers.google.com/speed/webp/docs/img2webp) binary when +it is available, falling back to picopt's internal algorithm using PIL & cwebp. ### SVG @@ -243,6 +237,54 @@ Or you can install svgo with npm: npm install -G svgo ``` +## ⚙️ Configuration + +Picopt layers configuration from these sources, lowest to highest priority: +packaged defaults, your user config file, a `-C CONFIG` file, per-directory +`.picopt.yaml` files (see below), environment variables, and command line +options. Every config file uses the same envelope: + + + +```yaml +picopt: + recurse: true + convert_to: [WEBP, CBZ] +``` + +### Per-directory `.picopt.yaml` files + +A `.picopt.yaml` placed inside a target directory applies to that directory and +everything under it. Files are discovered from each processed file's directory +up to the command line target root (never above it), and deeper directories win +over shallower ones. Environment variables and command line options still win +over every directory file. + + + +```sh +printf 'picopt:\n convert_to: [CBZ]\n' > comics/.picopt.yaml +picopt -rx CBR,CBZ . # CBRs under comics/ convert; siblings don't +``` + +Any config key is accepted and validated, but run-scoped keys — `dry_run`, +`list_only`, `timestamps`, `after`, `jobs`, `memory_limit`, `fail_fast`, +`fail_fast_container`, `verbose`, and `paths` — are governed by the run-level +value; setting them in a directory file has no per-directory effect. When +timestamps (`-t`) are enabled, editing, adding, or removing any `.picopt.yaml` +re-processes its tree on the next run. + +### Writing config files + +- `-w` / `--write-config` merges the options you invoked into your user config + file and then runs normally. +- `-W` / `--write-dir-config` writes them into a `.picopt.yaml` in each target + directory (a file target's parent). +- `--write-config-file PATH` writes them to an explicit path. + +Existing keys and comments in the target file survive the merge. Run-mode flags +(`-d`, `-L`, verbosity) and the target paths are never persisted. + ## ⌨️ Use Examples Optimize all JPEG files in a directory: @@ -288,6 +330,22 @@ timestamps. picopt -rStc CBZ,WEBP -x TIFF,CBR,CBZ /Volumes/Media/Comics ``` +Optimize the same comic library, but cap memory use so multi-GB archives don't +exhaust RAM: + + + +```sh +picopt -rStc CBZ,WEBP -x TIFF,CBR,CBZ --memory-limit 6G /Volumes/Media/Comics +``` + +picopt holds each archive plus its decompressed pages in memory while +optimizing, so large archives spread across many parallel workers can use a lot +of RAM. `--memory-limit` is an approximate peak-memory target (default: +two-thirds of detected RAM) that bounds how many large archives run at once — a +single archive bigger than the whole budget still runs, on its own — and `-j` +caps the number of parallel workers. + Optimize all files, but only JPEG format files: diff --git a/bin/benchmark.py b/bin/benchmark.py new file mode 100755 index 0000000..3e93c5d --- /dev/null +++ b/bin/benchmark.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +Benchmark picopt wall-clock time and peak RSS over synthetic corpora. + +Builds corpora from the test fixtures (an archive-heavy one and an +image-heavy one), runs picopt as a subprocess, and reports wall time and +the peak resident set size of the whole process tree. + +Usage: + uv run python bin/benchmark.py [--archives N] [--images N] [--keep] +""" + +import argparse +import re +import shutil +import subprocess +import sys +import time +from pathlib import Path +from tempfile import mkdtemp + +REPO_ROOT = Path(__file__).parent.parent +FIXTURES = REPO_ROOT / "tests" / "test_files" +CONTAINER_SRCS = ("test_cbz.cbz",) +IMAGE_SRCS = ( + "test_png.png", + "test_jpg.jpg", + "test_gif.gif", + "test_webp_lossless.webp", + "test_animated_gif.gif", +) +# /usr/bin/time peak-RSS line: bytes on darwin (-l), kilobytes on linux (-v). +_IS_DARWIN = sys.platform == "darwin" +_TIME_ARGS = ("/usr/bin/time", "-l" if _IS_DARWIN else "-v") +_RSS_RE = re.compile( + r"^\s*(\d+)\s+maximum resident set size" + if _IS_DARWIN + else r"Maximum resident set size[^:]*:\s*(\d+)", + re.MULTILINE, +) +_RSS_DIVISOR = 1024 * 1024 if _IS_DARWIN else 1024 + + +def _build_corpus(root: Path, n_archives: int, n_images: int) -> None: + archive_dir = root / "archives" + archive_dir.mkdir(parents=True) + for i in range(n_archives): + src = FIXTURES / "containers" / CONTAINER_SRCS[i % len(CONTAINER_SRCS)] + shutil.copy(src, archive_dir / f"comic_{i:04d}.cbz") + image_dir = root / "images" + image_dir.mkdir(parents=True) + for i in range(n_images): + src = FIXTURES / "images" / IMAGE_SRCS[i % len(IMAGE_SRCS)] + shutil.copy(src, image_dir / f"img_{i:04d}{src.suffix}") + + +def _run(args: tuple[str, ...], target: Path) -> tuple[float, int, int]: + """Run picopt over target; return (seconds, peak_rss_mib, returncode).""" + start = time.perf_counter() + proc = subprocess.run( # noqa: S603 + ( + *_TIME_ARGS, + sys.executable, + "-c", + "from picopt.cli import main; main()", + *args, + str(target), + ), + capture_output=True, + cwd=REPO_ROOT, + check=False, + ) + elapsed = time.perf_counter() - start + stderr = proc.stderr.decode(errors="replace") + match = _RSS_RE.search(stderr) + peak_mib = int(match.group(1)) // _RSS_DIVISOR if match else -1 + if proc.returncode: + print(proc.stdout.decode(errors="replace")[-2000:]) + print(stderr[-2000:]) + return elapsed, peak_mib, proc.returncode + + +def main() -> None: + """Build corpora and run the benchmark scenarios.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--archives", type=int, default=100) + parser.add_argument("--images", type=int, default=300) + parser.add_argument( + "--keep", action="store_true", help="Keep the corpus dir afterwards" + ) + opts = parser.parse_args() + + root = Path(mkdtemp(prefix="picopt-bench-")) + _build_corpus(root, opts.archives, opts.images) + print(f"corpus: {root} ({opts.archives} archives, {opts.images} images)") + + scenarios = ( + ("archives cold", ("-rx", "CBZ,PNG,JPEG,GIF,WEBP"), root / "archives"), + ("images cold", ("-rx", "PNG,JPEG,GIF,WEBP"), root / "images"), + ("images quiet", ("-q", "-rx", "PNG,JPEG,GIF,WEBP"), root / "images"), + ) + try: + print(f"{'scenario':<16} {'seconds':>8} {'peak MiB':>9} {'rc':>3}") + for name, args, target in scenarios: + elapsed, peak, rc = _run(args, target) + print(f"{name:<16} {elapsed:>8.2f} {peak:>9} {rc:>3}") + finally: + if opts.keep: + print(f"kept: {root}") + else: + shutil.rmtree(root, ignore_errors=True) + + +if __name__ == "__main__": + main() diff --git a/bin/roman.py b/bin/roman.py index 099357f..5819f92 100755 --- a/bin/roman.py +++ b/bin/roman.py @@ -116,7 +116,7 @@ def iter_files(path_strs: Sequence[str], spec: PathSpec) -> Generator[Path]: for path_str in path_strs: path = Path(path_str) if not path.exists(): - print(f"👎 Path does not exist: {path}", file=sys.stderr) # noqa: T201 + print(f"👎 Path does not exist: {path}", file=sys.stderr) sys.exit(2) root = path.resolve() @@ -163,13 +163,13 @@ def _parse_ignore_file(args: Namespace) -> PathSpec: if args.ignore_file: ignore_path = Path(args.ignore_file) if not ignore_path.is_file(): - print(f"👎 Can't read ignore file: {ignore_path}", file=sys.stderr) # noqa: T201 + print(f"👎 Can't read ignore file: {ignore_path}", file=sys.stderr) sys.exit(2) try: return build_ignore_spec(ignore_path) except OSError as exc: - print(f"👎 Failed to parse ignore file: {exc}", file=sys.stderr) # noqa: T201 + print(f"👎 Failed to parse ignore file: {exc}", file=sys.stderr) sys.exit(2) @@ -188,17 +188,17 @@ def main() -> None: continue if not has_description_comment(line2): - print(f"🔪 {path}") # noqa: T201 + print(f"🔪 {path}") offenders.append(path) if offenders: - print( # noqa: T201 + print( f"\n{len(offenders)} script(s) missing a description comment.", file=sys.stderr, ) sys.exit(1) - print("👍") # noqa: T201 + print("👍") if __name__ == "__main__": diff --git a/bun.lock b/bun.lock index 82859ba..3dbd860 100644 --- a/bun.lock +++ b/bun.lock @@ -6,35 +6,35 @@ "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.7.2", "@eslint/js": "^10.0.1", - "@eslint/json": "^1.2.0", + "@eslint/json": "^2.0.1", "@fsouza/prettierd": "^0.28.0", "@prettier/plugin-xml": "^3.4.2", "@stylistic/eslint-plugin": "^5.10.0", - "eslint": "^10.4.0", + "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-array-func": "^5.1.1", "eslint-plugin-compat": "^7.0.2", "eslint-plugin-de-morgan": "^2.1.2", "eslint-plugin-depend": "^1.5.0", "eslint-plugin-html": "^8.1.4", - "eslint-plugin-import-x": "^4.16.2", + "eslint-plugin-import-x": "^4.17.1", "eslint-plugin-math": "^0.13.1", - "eslint-plugin-mdx": "^3.7.1", + "eslint-plugin-mdx": "^3.8.1", "eslint-plugin-no-secrets": "^2.3.3", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-no-use-extend-native": "^0.7.3", - "eslint-plugin-package-json": "^1.1.0", - "eslint-plugin-perfectionist": "^5.9.0", + "eslint-plugin-package-json": "^1.5.0", + "eslint-plugin-perfectionist": "^5.9.1", "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-promise": "^7.3.0", - "eslint-plugin-regexp": "^3.1.0", - "eslint-plugin-security": "^4.0.0", - "eslint-plugin-sonarjs": "^4.0.3", - "eslint-plugin-toml": "^1.3.1", - "eslint-plugin-unicorn": "^64.0.0", - "eslint-plugin-yml": "^3.3.2", + "eslint-plugin-regexp": "^3.1.1", + "eslint-plugin-security": "^4.0.1", + "eslint-plugin-sonarjs": "^4.1.0", + "eslint-plugin-toml": "^1.4.0", + "eslint-plugin-unicorn": "^70.0.0", + "eslint-plugin-yml": "^3.5.0", "eslint_d": "^15.0.2", - "prettier": "^3.8.3", + "prettier": "^3.9.4", "prettier-plugin-nginx": "^1.0.3", "prettier-plugin-packagejson": "^3.0.2", "prettier-plugin-toml": "^2.0.6", @@ -56,7 +56,7 @@ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], @@ -82,11 +82,11 @@ "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" } }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="], - "@eslint/json": ["@eslint/json@1.2.0", "", { "dependencies": { "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanwhocodes/momoa": "^3.3.10", "natural-compare": "^1.4.0" } }, "sha512-CEFEyNgvzu8zn5QwVYDg3FaG+ZKUeUsNYitFpMYJAqoAlnw68EQgNbUfheSmexZr4n0wZPrAkPLuvsLaXO6wRw=="], + "@eslint/json": ["@eslint/json@2.0.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanwhocodes/momoa": "^3.3.10", "natural-compare": "^1.4.0" } }, "sha512-Thz2j92ceUF3Bq/0TuWb3MWn3Z+Cwc8k5ptF0fakl2D4Mp8mSx07Xr1aQM4R5NoihzarWUdxfOmQ8DesGy4jOg=="], "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="], "@fsouza/prettierd": ["@fsouza/prettierd@0.28.0", "", { "dependencies": { "core_d": "^6.1.1", "prettier": "^3.8.3" }, "optionalDependencies": { "@babel/parser": "^7.29.7", "@typescript-eslint/typescript-estree": "^8.60.0" }, "bin": { "prettierd": "bin/prettierd" } }, "sha512-XkRkIp2en2Lcm7Qc+zqNm8ClI3RArLCBNG2A4M2xzTw43S/vYSEDQ4NcfW+6ZsUyQeoVqrFcMU5H1yAKCVkv6A=="], @@ -120,8 +120,6 @@ "@ota-meshi/ast-token-store": ["@ota-meshi/ast-token-store@0.3.0", "", {}, "sha512-XRO0zi2NIUKq2lUk3T1ecFSld1fMWRKE6naRFGkgkdeosx7IslyUKNv5Dcb5PJTja9tHJoFu0v/7yEpAkrkrTg=="], - "@package-json/types": ["@package-json/types@0.0.12", "", {}, "sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw=="], - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@pkgr/core": ["@pkgr/core@0.3.6", "", {}, "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA=="], @@ -166,7 +164,7 @@ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.0", "@typescript-eslint/types": "^8.60.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1" } }, "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0" } }, "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ=="], @@ -174,7 +172,7 @@ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.60.0", "@typescript-eslint/tsconfig-utils": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="], @@ -278,8 +276,6 @@ "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], - "clean-regexp": ["clean-regexp@1.0.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5" } }, "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw=="], - "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -350,7 +346,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], + "eslint": ["eslint@10.6.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg=="], "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": "bin/cli.js" }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], @@ -358,7 +354,9 @@ "eslint-import-context": ["eslint-import-context@0.1.9", "", { "dependencies": { "get-tsconfig": "^4.10.1", "stable-hash-x": "^0.2.0" }, "peerDependencies": { "unrs-resolver": "^1.0.0" } }, "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg=="], - "eslint-mdx": ["eslint-mdx@3.7.1", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "espree": "^9.6.1 || ^10.4.0", "estree-util-visit": "^2.0.0", "remark-mdx": "^3.1.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "synckit": "^0.11.8", "unified": "^11.0.5", "unified-engine": "^11.2.2", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "eslint": ">=8.0.0", "remark-lint-file-extension": "*" }, "optionalPeers": ["remark-lint-file-extension"] }, "sha512-EBPfh1p4hHPX2o3Rd4UfpkfKrb+Bb/DkH0fZaR5XPug2G7gig43+d7Z2IWb+nqxjMlkD8qhjYLstb3NwNdhyyg=="], + "eslint-json-compat-utils": ["eslint-json-compat-utils@0.2.3", "", { "dependencies": { "esquery": "^1.6.0" }, "peerDependencies": { "@eslint/json": "*", "eslint": "*", "jsonc-eslint-parser": "^2.4.0 || ^3.0.0" }, "optionalPeers": ["@eslint/json"] }, "sha512-RbBmDFyu7FqnjE8F0ZxPNzx5UaptdeS9Uu50r7A+D7s/+FCX+ybiyViYEgFUaFIFqSWJgZRTpL5d8Kanxxl2lQ=="], + + "eslint-mdx": ["eslint-mdx@3.8.1", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "espree": "^9.6.1 || ^10.4.0 || ^11.2.0", "estree-util-visit": "^2.0.0", "remark-mdx": "^3.1.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "synckit": "^0.11.8", "unified": "^11.0.5", "unified-engine": "^11.2.2", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "eslint": ">=8.0.0", "remark-lint-file-extension": "*" }, "optionalPeers": ["remark-lint-file-extension"] }, "sha512-hnsqWwMOHqUANwxWEGt8XbwABPEr5sTOolAzqyUDFdlERpqjFE/icylb+mJl60VICL+kLbbvXWbnFLWZdTqJ2g=="], "eslint-plugin-array-func": ["eslint-plugin-array-func@5.1.1", "", { "peerDependencies": { "eslint": ">=8.51.0" } }, "sha512-TbVGk+yLqXHgtrS4DnYzg2Ycuk5y+lYFy5NgT748neQdJvNIYUucxp2QQjPU7dwbs9xp9fyktgtK069y9rNdig=="], @@ -370,11 +368,11 @@ "eslint-plugin-html": ["eslint-plugin-html@8.1.4", "", { "dependencies": { "htmlparser2": "^10.0.0" } }, "sha512-Eno3oPEj3s6AhvDJ5zHhnHPDvXp6LNFXuy3w51fNebOKYuTrfjOHUGwP+mOrGFpR6eOJkO1xkB8ivtbfMjbMjg=="], - "eslint-plugin-import-x": ["eslint-plugin-import-x@4.16.2", "", { "dependencies": { "@package-json/types": "^0.0.12", "@typescript-eslint/types": "^8.56.0", "comment-parser": "^1.4.1", "debug": "^4.4.1", "eslint-import-context": "^0.1.9", "is-glob": "^4.0.3", "minimatch": "^9.0.3 || ^10.1.2", "semver": "^7.7.2", "stable-hash-x": "^0.2.0", "unrs-resolver": "^1.9.2" }, "peerDependencies": { "@typescript-eslint/utils": "^8.56.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "eslint-import-resolver-node": "*" }, "optionalPeers": ["eslint-import-resolver-node"] }, "sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw=="], + "eslint-plugin-import-x": ["eslint-plugin-import-x@4.17.1", "", { "dependencies": { "@typescript-eslint/types": "^8.56.0", "comment-parser": "^1.4.1", "debug": "^4.4.1", "eslint-import-context": "^0.1.9", "is-glob": "^4.0.3", "minimatch": "^9.0.3 || ^10.1.2", "semver": "^7.7.2", "stable-hash-x": "^0.2.0", "unrs-resolver": "^1.9.2" }, "peerDependencies": { "@typescript-eslint/utils": "^8.56.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "eslint-import-resolver-node": "*" }, "optionalPeers": ["@typescript-eslint/utils", "eslint-import-resolver-node"] }, "sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg=="], "eslint-plugin-math": ["eslint-plugin-math@0.13.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "eslint-type-tracer": "^0.4.0" }, "peerDependencies": { "eslint": ">=8.57.0" } }, "sha512-vQSl7LpmgKggEKnkgzre+5zbFyuIDcqzlL/lXnTBRuA6T2/K0zzJ17gsie6+eip6X68r+PTlC2KpJYvHnWyp5g=="], - "eslint-plugin-mdx": ["eslint-plugin-mdx@3.7.1", "", { "dependencies": { "eslint-mdx": "^3.7.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0", "remark-mdx": "^3.1.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "synckit": "^0.11.8", "unified": "^11.0.5", "vfile": "^6.0.3" }, "peerDependencies": { "eslint": ">=8.0.0" } }, "sha512-G7Wqce+roWW35pQ/160UJiZdEWEyeR9LLaujDRnSV0O8SLHU6yHrK8C8kJLNtcBuU/AFd54zheefqZOyUr1aiA=="], + "eslint-plugin-mdx": ["eslint-plugin-mdx@3.8.1", "", { "dependencies": { "eslint-mdx": "^3.8.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0", "remark-mdx": "^3.1.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "synckit": "^0.11.8", "unified": "^11.0.5", "vfile": "^6.0.3" }, "peerDependencies": { "eslint": ">=8.0.0" } }, "sha512-4OLgotfBxUDc1f6ihXSagT/1+JCCUABA/2r6Kzl6gqFftg4dCV0wBfdwFo6X6UO/FzTHr3g6mVt+6prRXffc/Q=="], "eslint-plugin-no-secrets": ["eslint-plugin-no-secrets@2.3.3", "", { "peerDependencies": { "eslint": ">=5" } }, "sha512-sroCsCscpwQxZ/O9Ml69w5qt/2nvHhXDeyUnSqSC5dMANGxt4rQgUn7xbPvDmhbMPmUCAc2Y3SRU0cXet7kNWQ=="], @@ -382,25 +380,25 @@ "eslint-plugin-no-use-extend-native": ["eslint-plugin-no-use-extend-native@0.7.3", "", { "dependencies": { "is-get-set-prop": "^2.0.0", "is-js-type": "^3.0.0", "is-obj-prop": "^2.0.0", "is-proto-prop": "^3.0.1" }, "peerDependencies": { "eslint": "^9.3.0 || ^10.0.0" } }, "sha512-kYJhgZkiZIavu/wIwrO+n4GemQcMX53kWCNZNr7nGMkRD1aBFLkDpBivEYP7nIJINCo9fzPbFjrpeX5kr2Qbww=="], - "eslint-plugin-package-json": ["eslint-plugin-package-json@1.1.0", "", { "dependencies": { "@altano/repository-tools": "^2.0.1", "change-case": "^5.4.4", "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "eslint-fix-utils": "~0.4.1", "jsonc-eslint-parser": "^3.1.0", "package-json-validator": "^1.5.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.0", "sort-package-json": "^3.4.0" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-yFBkYxHBlGKBi3Av17pCkJie+G3/DN/XEy1KGkHnTLXCY8eX7rF2kGiFn8a3N4iIGV4NJzKA0HdpINwVxczX0Q=="], + "eslint-plugin-package-json": ["eslint-plugin-package-json@1.5.0", "", { "dependencies": { "@altano/repository-tools": "^2.0.1", "change-case": "^5.4.4", "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "eslint-fix-utils": "~0.4.1", "eslint-json-compat-utils": "^0.2.3", "jsonc-eslint-parser": "^3.1.0", "package-json-validator": "^1.5.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.0", "sort-package-json": "^4.0.0" }, "peerDependencies": { "@eslint/json": ">=1.0.0", "eslint": ">=9.0.0" }, "optionalPeers": ["@eslint/json"] }, "sha512-KQkAdn+7I4vn1W4Va+V8M94RhcgZBvgrxMglDqgEhJgQDRUJ9RdtrEiHoe/84oVTIlp0jiPttLYwkExdvHYhZQ=="], - "eslint-plugin-perfectionist": ["eslint-plugin-perfectionist@5.9.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.58.2", "natural-orderby": "^5.0.0" }, "peerDependencies": { "eslint": "^8.45.0 || ^9.0.0 || ^10.0.0" } }, "sha512-8TWzg02zmnBdZwCkWLi8jhzqXI+fE7Z/RwV8SL6xD45tJ8Bp3wGuYL2XtQgfe/Wd0eBqOUX+s6ey73IyszvKTA=="], + "eslint-plugin-perfectionist": ["eslint-plugin-perfectionist@5.9.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.61.0", "natural-orderby": "^5.0.0" }, "peerDependencies": { "eslint": "^8.45.0 || ^9.0.0 || ^10.0.0" } }, "sha512-30mHLNfEhzwaq5cquyWgnzrNXvT8AzwIwyeH5aj4U5ajhHSF2uiO6i09xpMDLv7koaZVTjLsvYF4m3gK/15tyA=="], "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.6", "", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.13" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ=="], "eslint-plugin-promise": ["eslint-plugin-promise@7.3.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" } }, "sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA=="], - "eslint-plugin-regexp": ["eslint-plugin-regexp@3.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "comment-parser": "^1.4.0", "jsdoc-type-pratt-parser": "^7.0.0", "refa": "^0.12.1", "regexp-ast-analysis": "^0.7.1", "scslre": "^0.3.0" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-qGXIC3DIKZHcK1H9A9+Byz9gmndY6TTSRkSMTZpNXdyCw2ObSehRgccJv35n9AdUakEjQp5VFNLas6BMXizCZg=="], + "eslint-plugin-regexp": ["eslint-plugin-regexp@3.1.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "comment-parser": "^1.4.0", "jsdoc-type-pratt-parser": "^7.0.0", "refa": "^0.12.1", "regexp-ast-analysis": "^0.7.1", "scslre": "^0.3.0" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-MxR5nqoQCtVWmJwia0D2+NlXX1xzdpkslsVOZLEYQ4PQWEaL65PCZXURxaBc3lPnkNFpNxzMIRmYVxdl8giXRA=="], - "eslint-plugin-security": ["eslint-plugin-security@4.0.0", "", { "dependencies": { "safe-regex": "^2.1.1" } }, "sha512-tfuQT8K/Li1ZxhFzyD8wPIKtlzZxqBcPr9q0jFMQ77wWAbKBVEhaMPVQRTMTvCMUDhwBe5vPVqQPwAGk/ASfxQ=="], + "eslint-plugin-security": ["eslint-plugin-security@4.0.1", "", { "dependencies": { "safe-regex": "^2.1.1" } }, "sha512-/lZCkOxPOWaf1jXAqgICrS8St3BMBccIPvhOSUYuV6VCr1o5nFVG998FnTLt6w2Nxb8Uo0nM8fzmnhp+GY/aEg=="], - "eslint-plugin-sonarjs": ["eslint-plugin-sonarjs@4.0.3", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "builtin-modules": "^3.3.0", "bytes": "^3.1.2", "functional-red-black-tree": "^1.0.1", "globals": "^17.4.0", "jsx-ast-utils-x": "^0.1.0", "lodash.merge": "^4.6.2", "minimatch": "^10.2.5", "scslre": "^0.3.0", "semver": "^7.7.4", "ts-api-utils": "^2.5.0", "typescript": ">=5" }, "peerDependencies": { "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0" } }, "sha512-5drkJKLC9qQddIiaATV0e8+ygbUc7b0Ti6VB7M2d3jmKNh3X0RaiIJYTs3dr9xnlhlrxo+/s1FoO3Jgv6O/c7g=="], + "eslint-plugin-sonarjs": ["eslint-plugin-sonarjs@4.1.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "builtin-modules": "^3.3.0", "bytes": "^3.1.2", "functional-red-black-tree": "^1.0.1", "globals": "^17.6.0", "jsx-ast-utils-x": "^0.1.0", "lodash.merge": "^4.6.2", "minimatch": "^10.2.5", "scslre": "^0.3.0", "semver": "^7.8.4", "ts-api-utils": "^2.5.0", "typescript": ">=5", "yaml": "^2.9.0" }, "peerDependencies": { "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0" } }, "sha512-rh+FlVz0yfd2RNIb6WqSkuGh0addX/Qi5scwQ5FphXDFrM6fZKcxP1+attJ78yUKcyYfiu6MTaISPpAFPzqRJw=="], - "eslint-plugin-toml": ["eslint-plugin-toml@1.3.1", "", { "dependencies": { "@eslint/core": "^1.0.1", "@eslint/plugin-kit": "^0.6.0", "@ota-meshi/ast-token-store": "^0.3.0", "debug": "^4.1.1", "toml-eslint-parser": "^1.0.1" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-1l00fBP03HIt9IPV7ZxBi7x0y0NMdEZmakL1jBD6N/FoKBvfKxPw5S8XkmzBecOnFBTn5Z8sNJtL5vdf9cpRMQ=="], + "eslint-plugin-toml": ["eslint-plugin-toml@1.4.0", "", { "dependencies": { "@eslint/core": "^1.0.1", "@eslint/plugin-kit": "^0.7.0", "@ota-meshi/ast-token-store": "^0.3.0", "debug": "^4.1.1", "toml-eslint-parser": "^1.0.1" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-3ErTnfUjXq/23f72XeyRcE0Y4Sd/ME1lsZeezczqpn2R4tE7+Sgco/NUKDXm0xAMz15tzcRz/9RfJRm6AqRO+A=="], - "eslint-plugin-unicorn": ["eslint-plugin-unicorn@64.0.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "@eslint-community/eslint-utils": "^4.9.1", "change-case": "^5.4.4", "ci-info": "^4.4.0", "clean-regexp": "^1.0.0", "core-js-compat": "^3.49.0", "find-up-simple": "^1.0.1", "globals": "^17.4.0", "indent-string": "^5.0.0", "is-builtin-module": "^5.0.0", "jsesc": "^3.1.0", "pluralize": "^8.0.0", "regexp-tree": "^0.1.27", "regjsparser": "^0.13.0", "semver": "^7.7.4", "strip-indent": "^4.1.1" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA=="], + "eslint-plugin-unicorn": ["eslint-plugin-unicorn@70.0.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "@eslint-community/eslint-utils": "^4.9.1", "browserslist": "^4.28.2", "change-case": "^5.4.4", "ci-info": "^4.4.0", "core-js-compat": "^3.49.0", "detect-indent": "^7.0.2", "find-up-simple": "^1.0.1", "globals": "^17.6.0", "indent-string": "^5.0.0", "is-builtin-module": "^5.0.0", "jsesc": "^3.1.0", "pluralize": "^8.0.0", "regjsparser": "^0.13.1", "semver": "^7.8.4", "strip-indent": "^4.1.1" }, "peerDependencies": { "eslint": ">=10.4" } }, "sha512-uAF9xMcVvvhTfvusCgogJ1wh4To3q2KhVMw3i1Apf/ILTbxsCjscvraAZACsEurb7no2fdXblD3whcbVnjw5zg=="], - "eslint-plugin-yml": ["eslint-plugin-yml@3.3.2", "", { "dependencies": { "@eslint/core": "^1.0.1", "@eslint/plugin-kit": "^0.7.0", "@ota-meshi/ast-token-store": "^0.3.0", "diff-sequences": "^29.0.0", "escape-string-regexp": "5.0.0", "natural-compare": "^1.4.0", "yaml-eslint-parser": "^2.0.0" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-XjmOB/fLBwYHqevnpclPL938V+9ExX7xw1hPaM3IPePGyMFRV1giS16RjSTNhIyCv/Oh0G0PEdmmZPATJ02YCw=="], + "eslint-plugin-yml": ["eslint-plugin-yml@3.5.0", "", { "dependencies": { "@eslint/core": "^1.0.1", "@eslint/plugin-kit": "^0.7.0", "@ota-meshi/ast-token-store": "^0.3.0", "diff-sequences": "^29.0.0", "escape-string-regexp": "5.0.0", "natural-compare": "^1.4.0", "yaml-eslint-parser": "^2.0.0" }, "peerDependencies": { "eslint": ">=9.38.0" } }, "sha512-u2UkSIp/+th1wYCt0QWeCI6agf24dxX6PbFfCCN18gGQHmXh3Cn9D/U5OiP5RNYTEwjXCLusj1OJRK+zwdvFqQ=="], "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], @@ -742,7 +740,7 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + "prettier": ["prettier@3.9.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg=="], "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], @@ -926,7 +924,7 @@ "sort-object-keys": ["sort-object-keys@2.1.0", "", {}, "sha512-SOiEnthkJKPv2L6ec6HMwhUcN0/lppkeYuN1x63PbyPRrgSPIuBJCiYxYyvWRTtjMlOi14vQUCGUJqS6PLVm8g=="], - "sort-package-json": ["sort-package-json@3.6.1", "", { "dependencies": { "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "git-hooks-list": "^4.1.1", "is-plain-obj": "^4.1.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.1", "tinyglobby": "^0.2.15" }, "bin": "cli.js" }, "sha512-Chgejw1+10p2D0U2tB7au1lHtz6TkFnxmvZktyBCRyV0GgmF6nl1IxXxAsPtJVsUyg/fo+BfCMAVVFUVRkAHrQ=="], + "sort-package-json": ["sort-package-json@4.0.0", "", { "dependencies": { "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "git-hooks-list": "^4.1.1", "is-plain-obj": "^4.1.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.1", "tinyglobby": "^0.2.15" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-6aYOlYI9AWioZ+rzu+4zKLmoFqJP0/fHDxrd7X04yqEibikY+5YVF0EYlyGn4v6X2PJY7yAUWV7oeP+i5rOm/g=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1046,7 +1044,7 @@ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "yaml": ["yaml@2.8.3", "", { "bin": "bin.mjs" }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "yaml-eslint-parser": ["yaml-eslint-parser@2.0.0", "", { "dependencies": { "eslint-visitor-keys": "^5.0.0", "yaml": "^2.0.0" } }, "sha512-h0uDm97wvT2bokfwwTmY6kJ1hp6YDFL0nRHwNKz8s/VD1FH/vvZjAKoMUE+un0eaYBSG7/c6h+lJTP+31tjgTw=="], @@ -1054,10 +1052,12 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@fsouza/prettierd/prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@npmcli/config/proc-log": ["proc-log@4.2.0", "", {}, "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA=="], @@ -1078,11 +1078,15 @@ "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="], - "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], + "@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.62.0", "", {}, "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg=="], + + "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ=="], "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.1", "@typescript-eslint/tsconfig-utils": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg=="], + "@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.62.0", "", {}, "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.62.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.62.0", "@typescript-eslint/tsconfig-utils": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A=="], "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="], @@ -1094,27 +1098,35 @@ "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - "core_d/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], - "eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "eslint/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - "eslint-plugin-perfectionist/@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA=="], + "eslint-mdx/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], + + "eslint-plugin-import-x/@typescript-eslint/types": ["@typescript-eslint/types@8.62.0", "", {}, "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg=="], + + "eslint-plugin-import-x/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "eslint-plugin-package-json/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "eslint-plugin-sonarjs/globals": ["globals@17.7.0", "", {}, "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg=="], + + "eslint-plugin-sonarjs/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "eslint-plugin-toml/@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], - "eslint-plugin-sonarjs/globals": ["globals@17.5.0", "", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="], + "eslint-plugin-unicorn/globals": ["globals@17.7.0", "", {}, "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg=="], - "eslint-plugin-unicorn/globals": ["globals@17.5.0", "", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="], + "eslint-plugin-unicorn/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "eslint-plugin-yml/@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], @@ -1138,10 +1150,14 @@ "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "prettier-plugin-packagejson/sort-package-json": ["sort-package-json@3.6.1", "", { "dependencies": { "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "git-hooks-list": "^4.1.1", "is-plain-obj": "^4.1.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.1", "tinyglobby": "^0.2.15" }, "bin": "cli.js" }, "sha512-Chgejw1+10p2D0U2tB7au1lHtz6TkFnxmvZktyBCRyV0GgmF6nl1IxXxAsPtJVsUyg/fo+BfCMAVVFUVRkAHrQ=="], + "prettier-plugin-xml/prettier": ["prettier@3.8.2", "", { "bin": "bin/prettier.cjs" }, "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q=="], "readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "sort-package-json/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1152,6 +1168,8 @@ "unified-engine/ignore": ["ignore@6.0.2", "", {}, "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A=="], + "unified-engine/yaml": ["yaml@2.8.3", "", { "bin": "bin.mjs" }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "vfile-reporter/supports-color": ["supports-color@9.4.0", "", {}, "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw=="], "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -1164,6 +1182,8 @@ "yaml-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "yaml-eslint-parser/yaml": ["yaml@2.8.3", "", { "bin": "bin.mjs" }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@npmcli/git/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], @@ -1176,19 +1196,17 @@ "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.1", "@typescript-eslint/types": "^8.58.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.62.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.62.0", "@typescript-eslint/types": "^8.62.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.62.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ=="], - "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - - "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="], + "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="], + "eslint-mdx/espree/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "eslint_d/eslint/@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="], @@ -1222,20 +1240,8 @@ "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], - - "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="], - - "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="], - - "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], - "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "npm-pick-manifest/npm-package-arg/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], } } diff --git a/cfg/gha_std.mk b/cfg/gha_std.mk index d66c0ea..3dbac93 100644 --- a/cfg/gha_std.mk +++ b/cfg/gha_std.mk @@ -2,4 +2,4 @@ DEVENV_GHA_STD := 1 export DEVENV_GHA_STD .PHONY: all -all: ; \ No newline at end of file +all:: ; \ No newline at end of file diff --git a/cfg/node_root.mk b/cfg/node_root.mk index 1420b6e..b12f6c1 100644 --- a/cfg/node_root.mk +++ b/cfg/node_root.mk @@ -3,4 +3,4 @@ export DEVENV_NODE_ROOT # Dummy target for mbake linting .PHONY: all -all: ; \ No newline at end of file +all:: ; \ No newline at end of file diff --git a/package.json b/package.json index c903d99..f9594db 100644 --- a/package.json +++ b/package.json @@ -60,11 +60,11 @@ "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.7.2", "@eslint/js": "^10.0.1", - "@eslint/json": "^1.2.0", + "@eslint/json": "^2.0.1", "@fsouza/prettierd": "^0.28.0", "@prettier/plugin-xml": "^3.4.2", "@stylistic/eslint-plugin": "^5.10.0", - "eslint": "^10.4.0", + "eslint": "^10.6.0", "eslint_d": "^15.0.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-array-func": "^5.1.1", @@ -72,23 +72,23 @@ "eslint-plugin-de-morgan": "^2.1.2", "eslint-plugin-depend": "^1.5.0", "eslint-plugin-html": "^8.1.4", - "eslint-plugin-import-x": "^4.16.2", + "eslint-plugin-import-x": "^4.17.1", "eslint-plugin-math": "^0.13.1", - "eslint-plugin-mdx": "^3.7.1", + "eslint-plugin-mdx": "^3.8.1", "eslint-plugin-no-secrets": "^2.3.3", "eslint-plugin-no-unsanitized": "^4.1.5", "eslint-plugin-no-use-extend-native": "^0.7.3", - "eslint-plugin-package-json": "^1.1.0", - "eslint-plugin-perfectionist": "^5.9.0", + "eslint-plugin-package-json": "^1.5.0", + "eslint-plugin-perfectionist": "^5.9.1", "eslint-plugin-prettier": "^5.5.6", "eslint-plugin-promise": "^7.3.0", - "eslint-plugin-regexp": "^3.1.0", - "eslint-plugin-security": "^4.0.0", - "eslint-plugin-sonarjs": "^4.0.3", - "eslint-plugin-toml": "^1.3.1", - "eslint-plugin-unicorn": "^64.0.0", - "eslint-plugin-yml": "^3.3.2", - "prettier": "^3.8.3", + "eslint-plugin-regexp": "^3.1.1", + "eslint-plugin-security": "^4.0.1", + "eslint-plugin-sonarjs": "^4.1.0", + "eslint-plugin-toml": "^1.4.0", + "eslint-plugin-unicorn": "^70.0.0", + "eslint-plugin-yml": "^3.5.0", + "prettier": "^3.9.4", "prettier-plugin-nginx": "^1.0.3", "prettier-plugin-packagejson": "^3.0.2", "prettier-plugin-toml": "^2.0.6", diff --git a/picopt/archive_stamps.py b/picopt/archive_stamps.py new file mode 100644 index 0000000..d2cc9f9 --- /dev/null +++ b/picopt/archive_stamps.py @@ -0,0 +1,70 @@ +"""Detached, picklable treestamps slice for worker-side archive walks.""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from treestamps import Treestamps + +if TYPE_CHECKING: + from treestamps.tree.config import TreestampsConfig + + +class ArchiveStamps: + """ + Read-only timestamps view an archive's unpack worker can use. + + Grovestamps holds unpicklable ruamel state, so archive handlers carry + this instead: the tree's config (picklable by design) plus a seed of + the archive's effective ancestor timestamp. A real, detached + :class:`Treestamps` is rebuilt lazily in the worker; it is never + ``set()`` or dumped, so it never opens a WAL or writes to disk. That + keeps the full treestamps semantics — ancestor-max lookups and + config-checked loading of ``.picopt_treestamps`` files found inside + archives — without duplicating them. + """ + + def __init__(self, tree_config: TreestampsConfig, seed: dict[str, Any]) -> None: + """ + Store the picklable ingredients; defer tree construction. + + ``seed`` is a ``Treestamps.dump_dict()`` mapping: config header + tags plus relative-path timestamp entries. The header must be + present (and match) when the config carries ``check_config=True``. + """ + self._tree_config = tree_config + self._seed = seed + self._tree: Treestamps | None = None + self.filename: str = Treestamps.get_filename(tree_config.program_name) + + def __getstate__(self) -> dict[str, Any]: + """Drop the lazily built tree; it holds unpicklable ruamel state.""" + state = self.__dict__.copy() + state["_tree"] = None + return state + + def _get_tree(self) -> Treestamps: + if self._tree is None: + tree = Treestamps(self._tree_config) + if self._seed: + tree.load_map(tree.root_dir, self._seed) + self._tree = tree + return self._tree + + def get_timestamp(self, top_path: Path, path: Path | str) -> float | None: + """Grovestamps-compatible ancestor-max timestamp lookup.""" + del top_path # one tree only; kept for Grovestamps API parity + return self._get_tree().get(path) + + def loads(self, path: str | Path, yaml_str: str | bytes) -> None: + """ + Load a timestamps yaml found inside the archive. + + ``path`` is the archive pseudo-path directory the yaml was found + in; relative entries anchor there. Passed straight to + ``Treestamps.loads`` (config checking included) — Grovestamps' + ``is_dir()`` re-anchoring would misfire on pseudo-paths that do + not exist on the filesystem. + """ + self._get_tree().loads(Path(path), yaml_str) diff --git a/picopt/archiveinfo.py b/picopt/archiveinfo.py index 8e9c62c..d056cb0 100644 --- a/picopt/archiveinfo.py +++ b/picopt/archiveinfo.py @@ -14,6 +14,9 @@ _DATETIME_ATTRGETTER = attrgetter( "year", "month", "day", "hour", "minute", "second", "microsecond" ) +# The zip format cannot represent timestamps before 1980; zipfile raises on +# them. Clamp older (or missing) member times to the epoch zipfile accepts. +_ZIP_EPOCH: Final = (1980, 1, 1, 0, 0, 0) ArchiveInfoType: TypeAlias = TarInfo | RarInfo | ZipInfo | SevenZipInfo @@ -32,6 +35,9 @@ class ArchiveInfo: def __init__(self, info: ArchiveInfoType) -> None: """Store the source info.""" self.info: ArchiveInfoType = info + # True when the member came from a real zip; repack then preserves + # its compression method instead of choosing one by content. + self.is_native_zipinfo: bool = isinstance(info, ZipInfo) self._filename: str | None = None self._is_dir: bool | None = None self._mtime: float | None = None @@ -117,14 +123,16 @@ def to_zipinfo(self) -> ZipInfo: case ZipInfo(): info = self.info case RarInfo(): - kwargs = {} - if filename := self.filename(): - kwargs["filename"] = filename - if self.info.date_time: - kwargs["date_time"] = self.info.date_time - info = ZipInfo(**kwargs) + filename = self.filename() or "NoName" + if date_time := self.info.date_time: + clamped = max(tuple(date_time), _ZIP_EPOCH) + info = ZipInfo(filename=filename, date_time=clamped) + else: + info = ZipInfo(filename=filename) case _: # TarInfo | SevenZipInfo - date_time = _DATETIME_ATTRGETTER(self.datetime()) + dttm = self.datetime() + date_time = _DATETIME_ATTRGETTER(dttm)[:6] if dttm else _ZIP_EPOCH + date_time = max(date_time, _ZIP_EPOCH) info = ZipInfo(filename=self.filename(), date_time=date_time) return info diff --git a/picopt/cli.py b/picopt/cli.py index fc3de93..1086b38 100644 --- a/picopt/cli.py +++ b/picopt/cli.py @@ -202,6 +202,7 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "-r", "--recurse", action="store_true", + default=None, dest="recurse", help="Recurse down through directories on the command line.", ) @@ -209,7 +210,7 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "-v", "--verbose", action="count", - default=1, + default=None, dest="verbose", help="Display more output. Can be used multiple times.", ) @@ -256,12 +257,14 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "-n", "--near-lossless", action="store_true", + default=None, dest="near_lossless", help="Precompress lossless WebP images with near lossless pixel adjustments.", ) parser.add_argument( "--png-max", action="store_true", + default=None, dest="png_max", help="Overzealously optimize pngs with -O5 and Zopfli.", ) @@ -269,6 +272,7 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "-S", "--no-symlinks", action="store_false", + default=None, dest="symlinks", help="Do not follow symlinks for files and directories", ) @@ -283,14 +287,15 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "-I", "--no-default-ignores", dest="ignore_defaults", - default=True, action="store_false", + default=None, help="Do not ignore dotfiles and sparsebundles.", ) parser.add_argument( "-b", "--bigger", action="store_true", + default=None, dest="bigger", help="Save optimized files that are larger than the originals", ) @@ -298,6 +303,7 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "-t", "--timestamps", action="store_true", + default=None, dest="timestamps", help="Record the optimization time in a timestamps file.", ) @@ -306,7 +312,7 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "--timestamps-no-check-config", dest="timestamps_check_config", action="store_false", - default=True, + default=None, help="Do not compare program config options with loaded timestamps.", ) parser.add_argument( @@ -314,7 +320,7 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "--timestamps-ignore-archive-entry-mtimes", dest="timestamps_ignore_archive_entry_mtimes", action="store_true", - default=False, + default=None, help="Use the archive file timestamp instead of archive entry timestamps.", ) parser.add_argument( @@ -328,6 +334,7 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "-d", "--dry_run", action="store_true", + default=None, dest="dry_run", help="Report how much would be saved, but do not replace files.", ) @@ -335,6 +342,7 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "-L", "--list", action="store_true", + default=None, dest="list_only", help="Only list files that would be optimized", ) @@ -342,6 +350,7 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "-M", "--strip-metadata", action="store_false", + default=None, dest="keep_metadata", help="Strip metadata like EXIF, XMP and ICC Profiles", ) @@ -353,6 +362,16 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: dest="jobs", help="Number of parallel jobs to run simultaneously.", ) + parser.add_argument( + "--memory-limit", + action="store", + dest="memory_limit", + help=( + "Approximate peak memory budget for optimizing large archives, " + "e.g. 8G or 512M. Limits how many big archives run at once so the " + "process isn't OOM-killed. 0 (default) means auto: two-thirds of RAM." + ), + ) parser.add_argument( "-C", "--config", @@ -364,6 +383,7 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: "-p", "--preserve", action="store_true", + default=None, help="Preserve file attributes (uid, gid, mode, mtime) after optimization.", ) parser.add_argument( @@ -375,15 +395,46 @@ def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: parser.add_argument( "--fail-fast", action="store_true", + default=None, dest="fail_fast", help="Stop all optimization on the first error encountered.", ) parser.add_argument( "--fail-fast-container", action="store_true", + default=None, dest="fail_fast_container", help="When an inner repack fails, fail the entire top-level container.", ) + parser.add_argument( + "-w", + "--write-config", + action="store_true", + default=None, + dest="write_config", + help=( + "Write the merged config to your user config file, then run " + "normally. Run-mode flags (dry run, list only, and verbosity) " + "are not persisted." + ), + ) + parser.add_argument( + "-W", + "--write-dir-config", + action="store_true", + default=None, + dest="write_dir_config", + help=( + "Write the invoked options into a .picopt.yaml in each target " + "directory (a file target's parent), then run normally." + ), + ) + parser.add_argument( + "--write-config-file", + action="store", + dest="write_config_file", + help="Write the merged config to the given file path, then run normally.", + ) parser.add_argument( "-V", "--version", @@ -415,9 +466,11 @@ def main(args: tuple[str, ...] | None = None) -> None: setup_logging(2) try: arguments = get_arguments(args) - setup_logging(arguments.picopt.verbose) + # Un-passed flags are None so config-file/env layers stay visible. + cli_verbose = arguments.picopt.verbose + setup_logging(1 if cli_verbose is None else cli_verbose) config = PicoptConfig().get_config(arguments) - walker = Walk(config) + walker = Walk(config, arguments) walker.walk() except ConfigError as err: logger.error(str(err)) diff --git a/picopt/config/__init__.py b/picopt/config/__init__.py index ac16fca..321e56b 100644 --- a/picopt/config/__init__.py +++ b/picopt/config/__init__.py @@ -9,12 +9,16 @@ from __future__ import annotations +import os import re +import sys import time +from contextlib import suppress from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Final from confuse import Configuration, MappingTemplate +from confuse.exceptions import ConfigError from confuse.templates import ( Choice, Integer, @@ -26,15 +30,20 @@ ) from dateutil.parser import parse from loguru import logger +from ruamel.yaml import YAML, YAMLError from picopt import PROGRAM_NAME from picopt import plugins as registry +from picopt.config.consts import DIR_CONFIG_FILENAME from picopt.config.handlers import ConfigHandlers from picopt.config.settings import ( ComputedSettings, IgnorePatterns, PicoptSettings, ) +from picopt.log import console + +__all__ = ("DIR_CONFIG_FILENAME", "PicoptConfig", "merge_config_file") if TYPE_CHECKING: from argparse import Namespace @@ -42,6 +51,26 @@ from confuse import Subview +# CLI args the config writers never persist: the write flags and -C INPUT +# path themselves, paths (argparse requires them on every invocation +# anyway), and the ephemeral run-mode flags dry_run / list_only / verbose — +# persisting those turns a one-off preview or -q into a permanent default +# (a sticky dry_run would make every future run a silent no-op). Users who +# truly want them as defaults can hand-edit the file; they are still +# honored on read. +_UNPERSISTED_ARGS: Final = frozenset( + { + "config", + "paths", + "write_config", + "write_dir_config", + "write_config_file", + "dry_run", + "list_only", + "verbose", + } +) + def _build_template() -> MappingTemplate: """ @@ -70,6 +99,7 @@ def _build_template() -> MappingTemplate: "jobs": Integer(), "keep_metadata": bool, "list_only": bool, + "memory_limit": Integer(), "near_lossless": bool, "paths": Sequence(ConfusePath()), "png_max": bool, @@ -84,11 +114,13 @@ def _build_template() -> MappingTemplate: MappingTemplate( { "handler_stages": dict, + # Patterns are None when the user disables the + # default ignores and supplies none (-I alone). "ignore": Optional( MappingTemplate( { - "case": re.Pattern, - "ignore_case": re.Pattern, + "case": Optional(re.Pattern), + "ignore_case": Optional(re.Pattern), } ) ), @@ -104,6 +136,142 @@ def _build_template() -> MappingTemplate: _MULTIPLE_STARS_RE: re.Pattern[str] = re.compile(r"\*+") _DEFAULT_IGNORE_REGEXPS = (r"^\.", r"\/\.", r"\.sparsebundle$") +# Memory-limit parsing / auto-detection. +_MEMORY_SUFFIXES: dict[str, int] = { + "K": 1024, + "M": 1024**2, + "G": 1024**3, + "T": 1024**4, +} +# Fraction of total RAM to budget by default. Two-thirds leaves ~a third for the +# OS, file cache, and headroom above the (approximate) per-archive estimate. +_DEFAULT_MEMORY_FRACTION = 2 / 3 +# Used only when total RAM can't be detected (e.g. no sysconf, no psutil). +_FALLBACK_TOTAL_RAM = 4 * 1024**3 + + +def _parse_memory_str(value: object) -> int | None: + """ + Parse a memory size (int bytes or a K/M/G/T-suffixed string) to bytes. + + Returns None for unparseable values so the caller can reject them + instead of silently falling back to auto-detection. + """ + if isinstance(value, (int, float)): + return int(value) + text = str(value).strip().upper().rstrip("B").strip() + if not text: + return None + multiplier = 1 + if text[-1] in _MEMORY_SUFFIXES: + multiplier = _MEMORY_SUFFIXES[text[-1]] + text = text[:-1].strip() + try: + return int(float(text) * multiplier) + except ValueError: + return None + + +def _detect_total_ram() -> int: + """Best-effort total physical RAM in bytes, cross-platform.""" + try: + # POSIX (Linux + macOS): total pages * page size. + return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") + except (ValueError, OSError, AttributeError): + pass + try: + import psutil + + return int(psutil.virtual_memory().total) + except Exception: + # Any psutil import/read failure falls back to a safe fixed budget. + return _FALLBACK_TOTAL_RAM + + +def _invoked_cli_options(nns: Namespace) -> dict: + """Return the options explicitly given on the command line.""" + # Unset options are None (argparse flag defaults are None so config + # layering works), so non-None values are exactly what was invoked. + # Tuples (from SplitArgsAction) become lists for clean YAML. + options = {} + for key, value in vars(nns).items(): + if value is None or key in _UNPERSISTED_ARGS: + continue + options[key] = list(value) if isinstance(value, tuple) else value + return options + + +def merge_config_file(target: Path, base_path: Path, options: dict) -> None: + """ + Merge ``options`` into ``base_path``'s config section and write to ``target``. + + Round-trip loads ``base_path`` so existing keys and comments survive, + then writes owner-only. Raises ``YAMLError`` or ``OSError`` on + read/write failure so callers decide whether it is fatal. + """ + yaml = YAML() + data = yaml.load(base_path.read_text()) if base_path.is_file() else None + if not isinstance(data, dict): + data = {} + section = data.get(PROGRAM_NAME) + if not isinstance(section, dict): + section = {} + data[PROGRAM_NAME] = section + section.update(options) + target.parent.mkdir(parents=True, exist_ok=True) + with target.open("w") as stream: + yaml.dump(data, stream) + # Best effort: filesystems without POSIX modes (e.g. FAT) just skip it. + with suppress(OSError): + target.chmod(0o600) + + +def _write_merged_config( + target: Path, base_path: Path, options: dict, verbose: int +) -> None: + """Write a config for the -w/-W/--write-config-file flags; fatal on failure.""" + try: + merge_config_file(target, base_path, options) + except (YAMLError, OSError) as exc: + logger.error(f"Could not write config file {target}: {exc}") + sys.exit(1) + # Confirm this explicitly-requested action at default verbosity, but + # honor -q (verbose 0) like every other user-facing message. + if verbose > 0: + console.print( + f"Wrote config to {target}", markup=False, highlight=False, soft_wrap=True + ) + + +def _target_dir_config_paths(paths: Iterable[str]) -> list[Path]: + """Deduped, order-stable ``.picopt.yaml`` path for each target directory.""" + targets: dict[Path, None] = {} + for path_str in paths: + path = Path(path_str) + # A file target's config lives in its parent directory (matches + # DirConfig's single-file-target boundary). + directory = path if path.is_dir() else path.parent + targets[directory / DIR_CONFIG_FILENAME] = None + return list(targets) + + +def _write_configs(config: Configuration, nns: Namespace) -> None: + """Persist the invoked options per the write flags.""" + options = _invoked_cli_options(nns) + verbose = config[PROGRAM_NAME]["verbose"].get(int) + # ``-C`` INPUT is the merge base for the user/explicit-path writes. + base = Path(nns.config) if nns.config else None + if nns.write_config: + target = Path(config.user_config_path()) + _write_merged_config(target, base or target, options, verbose) + if nns.write_config_file: + target = Path(nns.write_config_file) + _write_merged_config(target, base or target, options, verbose) + if nns.write_dir_config: + # Each directory config is updated in place; -C is not a base here. + for target in _target_dir_config_paths(nns.paths): + _write_merged_config(target, target, options, verbose) + class PicoptConfig(ConfigHandlers): """Construct Picopt Config.""" @@ -115,12 +283,37 @@ def _set_after(self, config: Subview) -> None: try: timestamp = float(after) except ValueError: - after_dt = parse(after) - timestamp = time.mktime(after_dt.timetuple()) + try: + after_dt = parse(after) + except (ValueError, OverflowError) as exc: + msg = f"Unparseable --after value {after!r}: {exc}" + raise ConfigError(msg) from exc + # datetime.timestamp() honors an explicit timezone; naive + # values are interpreted as local time, like mktime did. + timestamp = after_dt.timestamp() config["after"].set(timestamp) after = time.ctime(timestamp) logger.info(f"Optimizing after {after}") + def _set_memory_limit(self, config: Subview) -> None: + """ + Resolve the memory budget (in bytes) used to throttle large archives. + + Accepts an int or a K/M/G/T-suffixed string. ``0`` (the default) means + auto: two-thirds of detected physical RAM. The resolved value is always + a positive byte count. + """ + raw = config["memory_limit"].get() + limit = _parse_memory_str(raw) + if limit is None: + msg = f"Unparseable --memory-limit value: {raw!r}" + raise ConfigError(msg) + if limit <= 0: + limit = int(_detect_total_ram() * _DEFAULT_MEMORY_FRACTION) + config["memory_limit"].set(limit) + if config["verbose"].get(int) > 1: + logger.info(f"Memory budget for large archives: {limit // 1024**2} MiB") + @staticmethod def _get_ignore_regexp( ignore_list: list[str], @@ -196,25 +389,79 @@ def _set_timestamps(self, config: Subview) -> None: ts_str = "Not setting timestamps." logger.info(ts_str) - def get_config( - self, args: Namespace | None = None, modname: str = PROGRAM_NAME - ) -> PicoptSettings: - """Get the validated, frozen settings layered from defaults/env/args.""" + def _build_config( + self, + args: Namespace | None = None, + dir_config_files: tuple[Path, ...] = (), + modname: str = PROGRAM_NAME, + ) -> Configuration: + """ + Build a fully-layered, normalized confuse Configuration. + + Sources, lowest→highest priority: packaged defaults, user config, + the ``-C`` file, each directory ``.picopt.yaml`` + (shallowest→deepest), env vars, CLI args. Each ``set_*`` call + appends to the confuse source stack, so the directory files sit + above the user config yet below env/args — deeper directories win + over shallower ones, and env/CLI still win over every directory + file. Shared by :meth:`get_config` (no directory files) and + :class:`picopt.config.dirconfig.DirConfig` (per-directory chain). + """ config = Configuration(PROGRAM_NAME, modname=modname, read=False) config.read() if args and getattr(args, "picopt", None) and args.picopt.config: config.set_file(args.picopt.config) + for dir_config_file in dir_config_files: + config.set_file(str(dir_config_file)) config.set_env() if args: config.set_args(args) config_program = config[PROGRAM_NAME] self._set_ignore(config_program) self._set_after(config_program) + self._set_memory_limit(config_program) self._set_timestamps(config_program) self.set_format_handler_map(config_program) + return config + + @staticmethod + def _config_to_settings(config: Configuration) -> PicoptSettings: + """Validate against the template and convert to typed settings.""" ad = config.get(_build_template()) return _settings_from_attrdict(ad.picopt) + def get_config( + self, args: Namespace | None = None, modname: str = PROGRAM_NAME + ) -> PicoptSettings: + """Get the validated, frozen settings layered from defaults/env/args.""" + config = self._build_config(args, modname=modname) + # Validate (via _config_to_settings) before persisting so a bad + # command line can't poison a config file. get_config passes no + # directory files, so -w output round-trips via -C unchanged. + settings = self._config_to_settings(config) + nns = getattr(args, "picopt", None) if args else None + if nns is not None and ( + nns.write_config or nns.write_dir_config or nns.write_config_file + ): + _write_configs(config, nns) + return settings + + def get_dir_settings( + self, + args: Namespace | None, + dir_config_files: tuple[Path, ...], + ) -> PicoptSettings: + """ + Resolve settings with per-directory config files layered in. + + Like :meth:`get_config` but for the per-directory chain: the + directory ``.picopt.yaml`` files layer beneath env/CLI and above + the user config, and the write flags are never triggered. Used by + :class:`picopt.config.dirconfig.DirConfig`. + """ + config = self._build_config(args, dir_config_files) + return self._config_to_settings(config) + def _settings_from_attrdict(ad: Any) -> PicoptSettings: """ @@ -243,6 +490,7 @@ def _settings_from_attrdict(ad: Any) -> PicoptSettings: jobs=ad.jobs, keep_metadata=ad.keep_metadata, list_only=ad.list_only, + memory_limit=ad.memory_limit, near_lossless=ad.near_lossless, paths=tuple(ad.paths), png_max=ad.png_max, diff --git a/picopt/config/consts.py b/picopt/config/consts.py index ff389e9..7c7db93 100644 --- a/picopt/config/consts.py +++ b/picopt/config/consts.py @@ -10,6 +10,12 @@ from typing import Final +# Per-directory config files layer beneath env vars and CLI args but above +# the user config. Named to be distinct from the timestamps file +# ``.picopt_treestamps.yaml``. Lives here (a leaf module) so the walk layer +# can import it without triggering the config package's heavy import chain. +DIR_CONFIG_FILENAME: Final = ".picopt.yaml" + TIMESTAMPS_CONFIG_KEYS: Final[frozenset[str]] = frozenset( { "bigger", @@ -20,5 +26,8 @@ "near_lossless", "recurse", "symlinks", + # Changes which archive members are optimized, so flipping it must + # invalidate stamps like any other behavior-affecting option. + "timestamps_ignore_archive_entry_mtimes", } ) diff --git a/picopt/config/dirconfig.py b/picopt/config/dirconfig.py new file mode 100644 index 0000000..04d1548 --- /dev/null +++ b/picopt/config/dirconfig.py @@ -0,0 +1,147 @@ +"""Resolve per-directory settings from ``.picopt.yaml`` files.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import TYPE_CHECKING + +from confuse.exceptions import ConfigError +from loguru import logger +from ruamel.yaml import YAMLError + +from picopt.config.consts import DIR_CONFIG_FILENAME + +if TYPE_CHECKING: + from argparse import Namespace + from pathlib import Path + + from picopt.config import PicoptConfig + from picopt.config.settings import PicoptSettings + from picopt.log.summary import Stats + + +class DirConfig: + """ + Resolve the effective settings for a directory from ``.picopt.yaml`` files. + + Walks up the tree from a file's directory to the CLI target root + (``top_path``), collecting the ``.picopt.yaml`` files along the way, + and layers them beneath env vars and CLI args (via + :meth:`PicoptConfig.get_dir_settings`). Results are cached per + directory so cost is O(unique dirs), not O(files), and a fast path + returns the run-wide settings untouched when no directory config + applies. + """ + + # Run-mode keys are forced to the run-level values: a per-directory + # dry_run with run-level timestamps would stamp files that were never + # optimized, wrong-skipping them forever. Validated in the file per + # the full-schema decision, but governed at run level. + _RUN_LEVEL_KEYS = ("dry_run", "list_only", "timestamps") + + def __init__( + self, + picopt_config: PicoptConfig, + args: Namespace | None, + global_settings: PicoptSettings, + stats: Stats | None = None, + ) -> None: + """Initialize caches and the resolver dependencies.""" + self._picopt_config: PicoptConfig = picopt_config + self._args: Namespace | None = args + self._global_settings: PicoptSettings = global_settings + self._stats: Stats | None = stats + # Cache of each directory's readable config file (or None). + self._dir_config_files: dict[Path, Path | None] = {} + # Cache of resolved settings keyed by the file's directory. + self._settings: dict[Path, PicoptSettings] = {} + # Failure reasons already reported, so each is logged only once. + self._failed: set[str] = set() + + def _dir_config_file(self, path: Path) -> Path | None: + """Return ``path``'s readable ``.picopt.yaml``, or None. Cached.""" + if path not in self._dir_config_files: + config_file = path / DIR_CONFIG_FILENAME + # Use the run-wide symlinks setting to decide whether to read a + # config file: a file can't be trusted to say whether it should + # itself be read. + found = ( + config_file + if config_file.is_file() + and (self._global_settings.symlinks or not config_file.is_symlink()) + else None + ) + self._dir_config_files[path] = found + return self._dir_config_files[path] + + def _discover(self, top_path: Path, dir_path: Path) -> tuple[Path, ...]: + """ + Collect ``.picopt.yaml`` files from ``top_path`` down to ``dir_path``. + + Walks UP from ``dir_path`` to ``top_path`` (bounded at the CLI + target root so nothing above it is read), then reverses so the + result is shallowest→deepest — the order ``confuse.set_file`` + needs for deeper directories to win. + """ + files: list[Path] = [] + path = dir_path + # Check the boundary only after visiting the current dir so + # top_path's own config applies to nested paths too. + while True: + if (config_file := self._dir_config_file(path)) is not None: + files.append(config_file) + if path in (top_path, path.parent): + break + path = path.parent + files.reverse() + return tuple(files) + + def _record_failure(self, dir_files: tuple[Path, ...], exc: Exception) -> None: + """Log a directory-config failure once per offending file.""" + # confuse names the offending file in its message, and it is stable + # across descendant directories that re-read the same bad ancestor, + # so deduping on the reason logs each broken file exactly once. + reason = str(exc) + if reason in self._failed: + return + self._failed.add(reason) + # OSError carries the path; confuse/YAML errors put it in the reason. + config_file = getattr(exc, "filename", None) or dir_files[-1] + msg = f"Could not read directory config: {reason}" + logger.error(msg) + if self._stats is not None: + self._stats.record_error(config_file, msg) + + def _force_run_level_keys(self, settings: PicoptSettings) -> PicoptSettings: + """Pin run-mode keys to the run-level values.""" + overrides = { + key: getattr(self._global_settings, key) for key in self._RUN_LEVEL_KEYS + } + return replace(settings, **overrides) + + def get_settings(self, top_path: Path, dir_path: Path) -> PicoptSettings: + """ + Resolve the effective settings for ``dir_path``. + + Returns the run-wide settings unchanged when no ``.picopt.yaml`` + applies (fast path). Otherwise rebuilds a confuse Configuration + with the discovered directory files layered beneath env/CLI and + caches the result per directory. A malformed or unreadable + directory file is isolated: logged once, recorded in stats, and + the directory falls back to the run-wide settings so the walk + continues. + """ + if (cached := self._settings.get(dir_path)) is not None: + return cached + dir_files = self._discover(top_path, dir_path) + if not dir_files: + return self._global_settings + try: + settings = self._force_run_level_keys( + self._picopt_config.get_dir_settings(self._args, dir_files) + ) + except (ConfigError, YAMLError, OSError) as exc: + self._record_failure(dir_files, exc) + settings = self._global_settings + self._settings[dir_path] = settings + return settings diff --git a/picopt/config/handlers.py b/picopt/config/handlers.py index 729f31e..ee9f6db 100644 --- a/picopt/config/handlers.py +++ b/picopt/config/handlers.py @@ -18,17 +18,17 @@ The format → handler routing map is no longer built here at all — it lives in the registry as :func:`picopt.plugins.routes_by_format`. -Important ordering invariant: ``CWebPTool.probe()`` mutates -``WebPLossless.IS_MODERN_CWEBP`` as a side effect, and ``WebPLossless`` +Important ordering invariant: ``CWebPTool.probe()`` records on the +shared ``CWEBP_TOOL`` singleton whether cwebp is modern, and WebP handler instances read that flag in ``__init__`` to widen their accepted input formats. The probe loop runs at config-construction time, before -:class:`Walk` ever instantiates a handler, so the side effect is always in -place by the time it matters. Don't reorder. +:class:`Walk` ever instantiates a handler, so the flag is always in place +by the time it matters. Don't reorder. """ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from loguru import logger @@ -40,7 +40,19 @@ from confuse import Subview from picopt.plugins.base import Handler, Tool - from picopt.plugins.base.format import FileFormat + + +def _pick_tier_tool( + tier: tuple[Tool, ...], + disabled_program_names: frozenset[str], +) -> Tool | None: + """Return the first available, non-disabled tool in a tier.""" + for tool in tier: + if tool.name and tool.name in disabled_program_names: + continue + if tool.probe().available: + return tool + return None def _select_pipeline_for_handler( @@ -53,25 +65,15 @@ def _select_pipeline_for_handler( Returns ``None`` if any tier has no available tool — that signals the handler can't run on this machine. Returns the empty tuple for handlers with an empty pipeline (e.g. archive packers that use only stdlib / - library code); those are unconditionally available. + library code); those are unconditionally available. Tiers whose tools + are all optional are skipped when nothing in them is available. """ - if not handler_cls.PIPELINE: - return () chosen: list[Tool] = [] for tier in handler_cls.PIPELINE: - picked: Tool | None = None - for tool in tier: - if tool.name and tool.name in disabled_program_names: - continue - status = tool.probe() - if status.available: - picked = tool - break + picked = _pick_tier_tool(tier, disabled_program_names) if picked is not None: chosen.append(picked) - elif all(not tool.required for tool in tier): - continue - else: + elif any(tool.required for tool in tier): return None return tuple(chosen) @@ -134,39 +136,17 @@ def _set_format_handler_stages( if stages is not None: handler_stages[handler_cls] = stages - def _set_format_handled_strs_for_format( - self, - file_format: FileFormat, - all_format_strs: frozenset[str], - convert_chain: tuple[type[Handler], ...], - convert_to: frozenset[str | Any], - handler_stages: dict[type[Handler], tuple[Tool, ...]], - convert_format_strs: dict[Any, Any], - handled_format_strs: set[Any], - native: type[Handler] | None, - ) -> None: - if file_format.format_str not in all_format_strs: - return - picked_via_convert = False - for candidate in convert_chain: - if candidate.OUTPUT_FORMAT_STR not in convert_to: - continue - if candidate not in handler_stages: - continue - convert_format_strs.setdefault(candidate.OUTPUT_FORMAT_STR, set()).add( - file_format.format_str - ) - handled_format_strs.add(file_format.format_str) - picked_via_convert = True - break - if not picked_via_convert and native is not None and native in handler_stages: - handled_format_strs.add(file_format.format_str) - def set_format_handler_map(self, config: Subview) -> None: """Probe handlers for the requested formats and store availability.""" all_format_strs = self._get_config_set(config, "formats", "extra_formats") config["formats"].set(tuple(sorted(all_format_strs))) convert_to = self._get_config_set(config, "convert_to") + # Write the upcased lists back so template validation accepts + # lowercase user input for -x and -c exactly as it does for -f. + if extra_formats := self._get_config_set(config, "extra_formats"): + config["extra_formats"].set(tuple(sorted(extra_formats))) + if convert_to: + config["convert_to"].set(tuple(sorted(convert_to))) disabled_list: list[str] | None = config["disable_programs"].get(list) disabled_program_names = ( @@ -178,23 +158,32 @@ def set_format_handler_map(self, config: Subview) -> None: self._set_format_handler_stages( handler_cls, handler_stages, disabled_program_names ) - # Walk the routing table to compute the verbose-output summary. The - # routing layer will do this same lookup at runtime; we just mirror - # the result for the user-facing log. + # Build the verbose-output summary with the routing layer's own + # decision function so the log can never drift from what the walk + # actually does. repack=True for archives applies the same + # convert/CAN_PACK gates the repack pass will. handled_format_strs: set[str] = set() convert_format_strs: dict[str, set[str]] = {} routes = registry.routes_by_format() for file_format, (native, convert_chain) in routes.items(): - self._set_format_handled_strs_for_format( + if file_format.format_str not in all_format_strs: + continue + picked = registry.pick_route_handler( file_format, - all_format_strs, - convert_chain, - convert_to, - handler_stages, - convert_format_strs, - handled_format_strs, native, + convert_chain, + convert=True, + repack=file_format.archive, + convert_to=convert_to, + handler_stages=handler_stages, ) + if picked is None: + continue + handled_format_strs.add(file_format.format_str) + if picked is not native: + convert_format_strs.setdefault(picked.OUTPUT_FORMAT_STR, set()).add( + file_format.format_str + ) config["computed"]["handler_stages"].set(handler_stages) verbose: int = config["verbose"].get(int) diff --git a/picopt/config/settings.py b/picopt/config/settings.py index adbf062..793bf5c 100644 --- a/picopt/config/settings.py +++ b/picopt/config/settings.py @@ -15,7 +15,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING if TYPE_CHECKING: import re @@ -45,14 +45,15 @@ class ComputedSettings: - ``handler_stages`` is filled by :class:`ConfigHandlers` after probing each handler's PIPELINE; absence of a handler-class key signals - "no available pipeline" to the routing layer. Typed as ``dict[Any, Any]`` - to avoid an import cycle with ``picopt.plugins.base``; consumers know - the concrete shape via local ``cast``-style usage. + "no available pipeline" to the routing layer. Concretely it is + ``dict[type[Handler], tuple[Tool, ...]]``, but plugins.base imports + this module, and basedpyright's reportImportCycles counts even + TYPE_CHECKING edges, so the annotation stays structural. - ``ignore`` is filled by ``_set_ignore`` from the user's ``ignore`` list plus ``ignore_defaults``. """ - handler_stages: dict[Any, Any] + handler_stages: dict[type, tuple] ignore: IgnorePatterns @@ -76,6 +77,7 @@ class PicoptSettings: jobs: int keep_metadata: bool list_only: bool + memory_limit: int near_lossless: bool png_max: bool preserve: bool diff --git a/picopt/config_default.yaml b/picopt/config_default.yaml index 70c24df..881884f 100644 --- a/picopt/config_default.yaml +++ b/picopt/config_default.yaml @@ -12,6 +12,7 @@ picopt: jobs: 0 keep_metadata: True list_only: False + memory_limit: 0 near_lossless: False paths: [] png_max: False diff --git a/picopt/doctor.py b/picopt/doctor.py index ced6291..2b3a841 100644 --- a/picopt/doctor.py +++ b/picopt/doctor.py @@ -9,8 +9,8 @@ When a tool is missing, the doctor shows a platform-appropriate install command (brew on macOS, apt on Debian/Ubuntu, dnf on Fedora/RHEL). -Probing CWebPTool here also surfaces (via the WebPLossless.IS_MODERN_CWEBP -side effect) whether old or new cwebp behavior is in effect. +Probing CWebPTool also surfaces (via the probed instance's ``is_modern`` +flag) whether old or new cwebp behavior is in effect. """ from __future__ import annotations @@ -131,8 +131,6 @@ def _checkup_tool_get_tier_and_name( self, status, tier_idx: int, tool ) -> tuple[list[str], bool]: tier_has_available = False - if status.required: - self.total_required += 1 if status.available: tier_has_available = True prefix = "ok " @@ -140,7 +138,6 @@ def _checkup_tool_get_tier_and_name( else: prefix = "MISS" if status.required: - self.missing_required += 1 tier_color = "red" else: self.missing_optional += 1 @@ -160,8 +157,8 @@ def _checkup_tool_detail_bits(status, tool) -> list[str]: if not status.available and status.error: bits.extend(["-", f"[red]{escape(status.error)}[/red]"]) elif isinstance(tool, CWebPTool): - flag = "modern" if CWebPTool.IS_MODERN_CWEBP else "legacy" - flag_color = "green" if CWebPTool.IS_MODERN_CWEBP else "cyan" + flag = "modern" if tool.is_modern else "legacy" + flag_color = "green" if tool.is_modern else "cyan" bits.append(f"([{flag_color}]{flag}[/{flag_color}])") return bits @@ -187,6 +184,13 @@ def _checkup_handler_pipeline_tier(self, tier_idx: int, tier) -> None: tier_has_available = False for tool in tier: tier_has_available |= self._checkup_tool(tier_idx, tool) + # Tools within a tier are ALTERNATIVES: the tier is healthy when + # any one of them is available. Count requirements per tier, not + # per tool, or healthy installs report missing-required tools. + if any(tool.required for tool in tier): + self.total_required += 1 + if not tier_has_available: + self.missing_required += 1 if not tier_has_available: console.print( f"[yellow] !!! tier {tier_idx} has no available tool[/yellow]" @@ -208,18 +212,18 @@ def _checkup_plugin(self, plugin) -> None: self._checkup_handler(handler_cls) def _checkup_report(self) -> None: - available_tools = self.total_required - self.missing_required + available_tiers = self.total_required - self.missing_required required_color = "red" if self.missing_required else "green" optional_color = "cyan" if self.missing_optional else "green" summary = ( "Summary: " f"[{required_color}]" - f"{available_tools}/{self.total_required} required tools available" + f"{available_tiers}/{self.total_required} required tool tiers available" f"[/{required_color}], " f"[{required_color}]{self.missing_required} missing required" f"[/{required_color}], " - f"[{optional_color}]{self.missing_optional} missing optional" + f"[{optional_color}]{self.missing_optional} missing optional tools" f"[/{optional_color}]." ) console.print(summary) diff --git a/picopt/log/reporter.py b/picopt/log/reporter.py index 795ca60..fc1123e 100644 --- a/picopt/log/reporter.py +++ b/picopt/log/reporter.py @@ -11,6 +11,8 @@ from picopt.log.summary import Stats if TYPE_CHECKING: + from pathlib import Path + from picopt.report import ReportStats __all__ = ("Reporter",) @@ -46,15 +48,32 @@ def record_report(self, report: ReportStats) -> None: self.stats.record_bytes(report.bytes_in, bytes_out) kind = self._classify(report) - path = report.path - if path is not None: - recorder = getattr(self.stats, f"record_{kind}", None) - if recorder is not None: - recorder(path) - - getattr(self.progress, f"mark_{kind}")() + self._record_kind(kind, report.path) self._print_outcome(report.report_text(), kind) + def _record_kind(self, kind: str, path: Path | None) -> None: + """Dispatch to the explicit sink pair for a classified outcome.""" + match kind: + case "dry_run": + if path is not None: + self.stats.record_dry_run(path) + self.progress.mark_dry_run() + case "converted": + if path is not None: + self.stats.record_converted(path) + self.progress.mark_converted() + case "saved": + if path is not None: + self.stats.record_saved(path) + self.progress.mark_saved() + case "lost": + if path is not None: + self.stats.record_lost(path) + self.progress.mark_lost() + case _: + msg = f"Unknown report outcome kind: {kind}" + raise ValueError(msg) + @staticmethod def _classify(report: ReportStats) -> str: if report.test: diff --git a/picopt/path.py b/picopt/path.py index 2a87ad3..180f7e0 100644 --- a/picopt/path.py +++ b/picopt/path.py @@ -44,7 +44,23 @@ def is_path_ignored(config: PicoptSettings, path: Path, *, ignore_case: bool) -> class PathInfo: - """Path Info object, mostly for passing down walk.""" + """ + The universal work item: one optimizable thing, wherever it lives. + + Filesystem files, archive members, animation frames, and synthetic PDF + JPEG streams all flow through the same detection → handler → report + pipeline as PathInfos. + + Mutable-identity contract: a PathInfo is created once per item and its + IDENTITY is shared across stages — including pickling into a worker + and back. After a worker optimizes an in-container entry, the + scheduler mutates the main process's original via + ``hydrate_optimized_path_info`` (``set_data``/``rename``) so the + parent container repacks the updated entry. Most read accessors cache + lazily; ``rename()`` invalidates the name-derived caches and + ``set_data()`` replaces the payload — add matching invalidation if a + new cached field depends on either. + """ _UNSET = object() @@ -98,6 +114,7 @@ def __init__( # noqa: PLR0913 path: Path | None = None, frame: int | None = None, archiveinfo: ZipInfo | RarInfo | TarInfo | SevenZipInfo | None = None, + archive_index: int | None = None, data: bytes | None = None, noop: bool = False, ) -> None: @@ -122,10 +139,16 @@ def __init__( # noqa: PLR0913 self.archiveinfo: ArchiveInfo | None = ( ArchiveInfo(archiveinfo) if archiveinfo else None ) + # Position within the parent archive; repack uses it to preserve + # member order (EPUB requires mimetype first). + self.archive_index: int | None = archive_index # optionally computed self._data: bytes | None = data self._header_bytes: bytes | None = None + # (FileFormat | None, info-Mapping) cached by unpack workers via + # predetect_format(); None means detection has not run yet. + self.detected: tuple | None = None # always computed self._is_dir: bool | None = None diff --git a/picopt/pillow/png_bit_depth.py b/picopt/pillow/png_bit_depth.py index 7200cea..314bb63 100755 --- a/picopt/pillow/png_bit_depth.py +++ b/picopt/pillow/png_bit_depth.py @@ -17,14 +17,13 @@ def png_bit_depth(buffer: BinaryIO) -> int | None: """If a file is a png, get the bit depth from the standard position.""" - if _PNG_HEADER.compare(buffer): - buffer.seek(_BIT_DEPTH_OFFSET) # bit depth offset - depth = buffer.read(1) - result = int.from_bytes(depth, byteorder="little") - else: + if not _PNG_HEADER.compare(buffer): reason = "cannot find bit depth of non png" raise ValueError(reason) - return result + buffer.seek(_BIT_DEPTH_OFFSET) # bit depth offset + depth = buffer.read(1) + # None for truncated files rather than a fake 0 depth. + return depth[0] if depth else None def main() -> None: diff --git a/picopt/pillow/webp_lossless.py b/picopt/pillow/webp_lossless.py index b13787a..7ad4744 100755 --- a/picopt/pillow/webp_lossless.py +++ b/picopt/pillow/webp_lossless.py @@ -2,48 +2,60 @@ """ Determine if a webp is lossless. -This should be a part of Pillow -https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification +Parses the RIFF chunk structure rather than searching for byte strings, so +metadata payloads (ICC, EXIF, XMP) can neither hide the image chunk nor +fake one: +https://developers.google.com/speed/webp/docs/riff_container + +This should be a part of Pillow. """ -from io import BufferedReader, BytesIO -from mmap import ACCESS_READ, mmap from pathlib import Path +from struct import unpack +from typing import BinaryIO -from picopt.pillow.header import ImageHeader - -# RIFF_HEADER = ImageHeader(0, b"RIFF")) -# WEBP_HEADER = ImageHeader(8, b"WEBP")) -_VP8_HEADER = ImageHeader(12, b"VP8") -_VP8L_HEADER = b"VP8L" -_SEARCH_LEN = 128 +_RIFF_HEADER_LEN = 12 +_CHUNK_HEADER_LEN = 8 +# ANMF chunk: 16 bytes of frame parameters, then the frame's image chunks. +_ANMF_FRAME_PARAMS_LEN = 16 +_LOSSLESS_FOURCC = b"VP8L" +_LOSSY_FOURCC = b"VP8 " +_ANIM_FRAME_FOURCC = b"ANMF" -def is_lossless(input_buffer: BytesIO | BufferedReader) -> bool: - """Compare header types against lossless types.""" - buffer: BytesIO | mmap = ( - mmap(input_buffer.fileno(), 0, access=ACCESS_READ) - if isinstance(input_buffer, BufferedReader) - else input_buffer - ) +def is_lossless(input_buffer: BinaryIO) -> bool: + """ + Walk RIFF chunks until the first image data chunk decides losslessness. - try: - if not _VP8_HEADER.compare(buffer): - return False - x = buffer.read(1) - if x == b"L": - return True - if x == b"X": - finder = ( - buffer - if isinstance(buffer, mmap) - else bytearray(buffer.read(_SEARCH_LEN)) - ) - return finder.find(_VP8L_HEADER) != -1 + Leaves the buffer position at 0; the caller owns closing it. + """ + input_buffer.seek(0) + header = input_buffer.read(_RIFF_HEADER_LEN) + if ( + len(header) < _RIFF_HEADER_LEN + or header[:4] != b"RIFF" + or header[8:12] != b"WEBP" + ): return False + try: + while True: + chunk_header = input_buffer.read(_CHUNK_HEADER_LEN) + if len(chunk_header) < _CHUNK_HEADER_LEN: + return False + fourcc = chunk_header[:4] + (size,) = unpack(" None: diff --git a/picopt/plugins/__init__.py b/picopt/plugins/__init__.py index 9999fd7..9f4bb5d 100644 --- a/picopt/plugins/__init__.py +++ b/picopt/plugins/__init__.py @@ -23,10 +23,10 @@ from functools import cache from typing import TYPE_CHECKING -from picopt.plugins.base import Detector, Handler, Plugin +from picopt.plugins.base import ContainerHandler, Detector, Handler, Plugin if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Iterator, Mapping from picopt.plugins.base.format import FileFormat @@ -43,10 +43,10 @@ def _discover() -> tuple[Plugin, ...]: plugins: list[Plugin] = [] for module_info in pkgutil.iter_modules(plugins_pkg.__path__): - if module_info.ispkg: - continue # skip the base/ subpackage - if module_info.name.startswith("_"): + if module_info.name.startswith("_") or module_info.name == "base": continue + # Plugins may be single modules or subpackages (e.g. webp/); + # either way the PLUGIN descriptor lives at the top level. module = importlib.import_module(f"picopt.plugins.{module_info.name}") plugin = getattr(module, "PLUGIN", None) if isinstance(plugin, Plugin): @@ -183,3 +183,72 @@ def detectors() -> tuple[type[Detector], ...]: plugin.detector for plugin in iter_plugins() if plugin.detector is not None ] return tuple(sorted(found, key=lambda d: -d.PRIORITY)) + + +def is_pipeline_available(handler_cls: type[Handler], handler_stages: Mapping) -> bool: + """ + Whether the config-time probe found a workable pipeline for a handler. + + A handler is "available" iff every tier in its ``PIPELINE`` produced a + selected tool. Handlers with an empty PIPELINE (e.g. archive handlers + that pack via Python libraries, or PILPack sentinels) are always + available — there is nothing to be missing. + """ + if not handler_cls.PIPELINE: + return True + return handler_cls in handler_stages + + +def pick_route_handler( + file_format: FileFormat | None, + native: type[Handler] | None, + convert_chain: tuple[type[Handler], ...], + *, + convert: bool, + repack: bool, + convert_to: frozenset[str], + handler_stages: Mapping, +) -> type[Handler] | None: + """ + Pick the handler class for a format — the one routing decision. + + Used by the factory at runtime and by the config layer to build the + startup formats summary, so the log can never drift from what the + walk actually does. Selection: the first convert-chain candidate whose + output the user asked for and whose pipeline probed available (archives + only convert during the repack pass), else the native handler if + available; repack callers additionally require a packing container. + """ + if file_format is None: + return None + handler_cls: type[Handler] | None = None + if convert and (not file_format.archive or repack): + handler_cls = _pick_convert_handler(convert_chain, convert_to, handler_stages) + if ( + handler_cls is None + and native is not None + and is_pipeline_available(native, handler_stages) + ): + handler_cls = native + if ( + repack + and handler_cls is not None + and not (issubclass(handler_cls, ContainerHandler) and handler_cls.CAN_PACK) + ): + handler_cls = None + return handler_cls + + +def _pick_convert_handler( + convert_chain: tuple[type[Handler], ...], + convert_to: frozenset[str], + handler_stages: Mapping, +) -> type[Handler] | None: + """First convert candidate the user asked for whose pipeline is available.""" + for candidate in convert_chain: + if candidate.OUTPUT_FORMAT_STR not in convert_to: + continue + if not is_pipeline_available(candidate, handler_stages): + continue + return candidate + return None diff --git a/picopt/plugins/base/archive.py b/picopt/plugins/base/archive.py index 59356f4..ab64f8b 100644 --- a/picopt/plugins/base/archive.py +++ b/picopt/plugins/base/archive.py @@ -70,7 +70,9 @@ def _archive_readfile(self, archive, archiveinfo) -> bytes: def _get_archive(self): """Open the archive for reading.""" archive_class = type(self).ARCHIVE_CLASS - archive = archive_class(self.original_path, "r") + # In-archive members have no filesystem path; open from the buffer. + target = self.path_info.path_or_buffer() + archive = archive_class(target, "r") if not archive: msg = f"Unknown archive type: {self.original_path}" raise ValueError(msg) @@ -92,8 +94,14 @@ def _pack_info_one_file(self, archive, path_info) -> None: raise NotImplementedError(msg) def _archive_write(self, archive) -> None: - while self._optimized_contents: - path_info = self._optimized_contents.pop() + # Preserve the source archive's member order (EPUB requires its + # mimetype entry first); members without an index sort last, stably. + contents = sorted( + self._optimized_contents, + key=lambda pi: (pi.archive_index is None, pi.archive_index or 0), + ) + self._optimized_contents.clear() + for path_info in contents: self._pack_info_one_file(archive, path_info) if self.comment: archive.comment = self.comment @@ -121,27 +129,27 @@ def identify_format(cls, path_info: PathInfo) -> FileFormat | None: # --------------------------------------------------------------- walk - def _create_path_info(self, archiveinfo, data: bytes | None = None): - + def _create_path_info( + self, archiveinfo, index: int | None = None, data: bytes | None = None + ): return PathInfo( path_info=self.path_info, archiveinfo=archiveinfo, + archive_index=index, data=data, convert=self._convert_children, container_parents=self.path_info.container_path_history(), ) def _is_archive_path_skip(self, path_info) -> bool: - return bool(self._skipper) and ( - self._skipper.is_walk_file_skip(path_info) - or ( - not self.config.timestamps_ignore_archive_entry_mtimes - and self._skipper.is_older_than_timestamp(path_info) - ) + skipper = self._get_skipper() + return skipper.is_walk_file_skip(path_info) or ( + not self.config.timestamps_ignore_archive_entry_mtimes + and skipper.is_older_than_timestamp(path_info) ) - def _walk_one_entry(self, archive, archiveinfo): - path_info = self._create_path_info(archiveinfo) + def _walk_one_entry(self, archive, archiveinfo, index: int): + path_info = self._create_path_info(archiveinfo, index) if self._is_archive_path_skip(path_info): self._skip_path_infos.add(path_info) return None @@ -168,7 +176,7 @@ def _consume_archive_timestamps(self, archive): yaml_str = yaml_str.decode(errors="replace") archive_sub_path = self.path_info.archive_pseudo_path() / path.parent self._timestamps.loads(archive_sub_path, yaml_str) - if self._skipper and self.config.verbose > 1: + if self.config.verbose > 1: logger.info(f"Consumed picopt timestamp in archive: {path}") self._do_repack = True return tuple(non_treestamp_entries) @@ -189,9 +197,10 @@ def walk(self) -> Generator[PathInfo]: if self.config.verbose > 1: logger.info(f"Scanning archive {self.path_info.full_output_name()}…") with self._get_archive() as archive: + self._set_comment(archive) non_treestamp_entries = self._consume_archive_timestamps(archive) - for archiveinfo in non_treestamp_entries: - if path_info := self._walk_one_entry(archive, archiveinfo): + for index, archiveinfo in enumerate(non_treestamp_entries): + if path_info := self._walk_one_entry(archive, archiveinfo, index): yield path_info # Always copy unchanged files: the scheduler decides # whether to repack after children are processed, but diff --git a/picopt/plugins/base/container.py b/picopt/plugins/base/container.py index 6091fd7..f821989 100644 --- a/picopt/plugins/base/container.py +++ b/picopt/plugins/base/container.py @@ -34,9 +34,9 @@ if TYPE_CHECKING: from collections.abc import Generator + from pathlib import Path - from treestamps import Grovestamps - + from picopt.archive_stamps import ArchiveStamps from picopt.config.settings import PicoptSettings from picopt.path import PathInfo from picopt.report import ReportStats @@ -52,7 +52,7 @@ class ContainerHandler(Handler, ABC): def __init__( self, *args: Any, - timestamps: Grovestamps | None = None, + timestamps: ArchiveStamps | None = None, repack_handler_class: type[ContainerHandler] | None = None, comment: bytes | None = None, optimized_contents: set[PathInfo] | None = None, @@ -62,20 +62,29 @@ def __init__( super().__init__(*args, **kwargs) # Config gets mutated for pickling protection during repack. self.config: PicoptSettings = copy(self.config) - self._timestamps: Grovestamps | None = timestamps + # A picklable slice of the run's treestamps: walk() executes in a + # worker process, so member skip checks and in-archive timestamp + # consumption must survive the pickle round trip. + self._timestamps: ArchiveStamps | None = timestamps self.repack_handler_class: type[ContainerHandler] | None = repack_handler_class - # Lazy import to avoid a cycle (walk.skip imports nothing of ours). - from picopt.walk.skip import WalkSkipper - - # Workers don't share Reporter with the parent process; give each - # worker a detached one so skip-side counters/marks become no-ops. - self._skipper: WalkSkipper | None = WalkSkipper( - self.config, Reporter(), timestamps, in_archive=True - ) + self._skipper: WalkSkipper | None = None self.comment: bytes | None = comment self._optimized_contents: set[PathInfo] = optimized_contents or set() self._do_repack: bool = bool(optimized_contents) + def _get_skipper(self) -> WalkSkipper: + """Build the walk skipper lazily; workers rebuild after pickling.""" + if self._skipper is None: + # Lazy import to avoid a cycle (walk.skip imports nothing of ours). + from picopt.walk.skip import WalkSkipper + + # Workers don't share Reporter with the parent process; give each + # worker a detached one so skip-side counters/marks become no-ops. + self._skipper = WalkSkipper( + self.config, Reporter(), self._timestamps, in_archive=True + ) + return self._skipper + # ----------------------------------------------------------- walk/unpack @abstractmethod @@ -85,7 +94,7 @@ def walk(self) -> Generator[PathInfo]: def _walk_finish(self) -> None: if self.config.verbose < 2: # noqa: PLR2004 return - if self._do_repack and self._skipper: + if self._do_repack: logger.info(f"Optimizing contents in {self.path_info.full_output_name()}") else: msg = ( @@ -124,6 +133,16 @@ def get_optimized_contents( """Return optimized contents.""" return self._optimized_contents + def get_staging_dir(self) -> Path | None: + """ + Return the on-disk staging dir walk() created, if any. + + The scheduler reads this off the pickle-roundtripped handler after + unpack so it can rmtree the staging on rollback, fail-fast, and + final cleanup — pack_into() only cleans it on the success path. + """ + return None + # ------------------------------------------------------------- packing def pack_into(self) -> BinaryIO: @@ -147,9 +166,9 @@ def optimize(self) -> BinaryIO: return self.pack_into() def __getstate__(self) -> dict[str, Any]: - """Drop Grovestamps for worker handoff; its ruamel.yaml Reader owns an un-picklable BufferedReader.""" + """Drop the skipper for worker handoff; it rebuilds lazily.""" state = self.__dict__.copy() - state["_timestamps"] = None + # _timestamps is an ArchiveStamps and manages its own picklability. state["_skipper"] = None return state diff --git a/picopt/plugins/base/handler.py b/picopt/plugins/base/handler.py index 05259a8..5758117 100644 --- a/picopt/plugins/base/handler.py +++ b/picopt/plugins/base/handler.py @@ -25,6 +25,8 @@ from types import MappingProxyType from typing import TYPE_CHECKING, BinaryIO, Final +from loguru import logger + from picopt import WORKING_SUFFIX from picopt.path import DOUBLE_SUFFIX, PathInfo from picopt.plugins.base.format import FileFormat @@ -139,20 +141,22 @@ def run_ext_fs( input_buffer.seek(0) input_tmp_file.write(input_buffer.read()) - proc = subprocess.run( # noqa: S603 - args, - check=True, - capture_output=True, - ) - - if input_path_tmp and input_path: - input_path.unlink(missing_ok=True) - - if output_path_tmp and output_path: - output_buffer = BytesIO(output_path.read_bytes()) - output_path.unlink(missing_ok=True) - else: - output_buffer = BytesIO(proc.stdout) + try: + proc = subprocess.run( # noqa: S603 + args, + check=True, + capture_output=True, + ) + if output_path_tmp and output_path: + output_buffer = BytesIO(output_path.read_bytes()) + else: + output_buffer = BytesIO(proc.stdout) + finally: + # Temp files must not survive a failed external tool. + if input_path_tmp and input_path: + input_path.unlink(missing_ok=True) + if output_path_tmp and output_path: + output_path.unlink(missing_ok=True) return output_buffer def get_working_path(self) -> Path: @@ -247,16 +251,28 @@ def _get_buffer_len(self, buffer: BinaryIO) -> int: def _save_new_data(self, final_data_buffer: BinaryIO | None) -> bytes: if final_data_buffer is None: return b"" - if isinstance(final_data_buffer, BytesIO) or bool(self.path_info.archiveinfo): - final_data_buffer.seek(0) - return final_data_buffer.read() - return b"" + # Only in-container entries need their bytes returned — the parent's + # repack consumes them. Top-level results are already written to + # disk; returning them would pickle the whole file back to the main + # process just to be discarded. + if self.path_info.path is not None: + return b"" + final_data_buffer.seek(0) + return final_data_buffer.read() def _write_final_path(self, final_data_buffer: BinaryIO) -> None: if isinstance(final_data_buffer, BytesIO): - with self.working_path.open("wb") as working_path: + # Never write into original_path directly: a crash or full disk + # mid-write would destroy the user's file. Write to a sibling + # temp file (same filesystem, so replace() below is atomic); + # stale ones are cleaned by WalkSkipper on the next run. + tmp_path = self.final_path.with_name(self.final_path.name + WORKING_SUFFIX) + with tmp_path.open("wb") as tmp_file: final_data_buffer.seek(0) - working_path.write(final_data_buffer.read()) + tmp_file.write(final_data_buffer.read()) + tmp_file.flush() + os.fsync(tmp_file.fileno()) + self.working_path = tmp_path self.working_path.replace(self.final_path) def _cleanup_original_path(self) -> None: @@ -274,7 +290,12 @@ def _preserve_stats(self) -> None: stat = self.path_info.stat() if stat is None: return - os.chown(self.final_path, stat.st_uid, stat.st_gid) + try: + os.chown(self.final_path, stat.st_uid, stat.st_gid) + except OSError as exc: + # Non-root users can't chown; a successful optimization must + # not become an error, and mode/mtime should still restore. + logger.debug(f"Could not restore owner of {self.final_path}: {exc}") self.final_path.chmod(stat.st_mode) os.utime(self.final_path, ns=(stat.st_atime_ns, stat.st_mtime_ns)) @@ -288,11 +309,9 @@ def _cleanup_filesystem(self, final_data_buffer: BinaryIO) -> None: self._preserve_stats() def _cleanup_after_optimize_get_return_data( - self, final_data_buffer: BinaryIO, bytes_in: int, bytes_out: int + self, final_data_buffer: BinaryIO, *, replaced: bool ) -> bytes: - if not self.config.dry_run and ( - bytes_out > 0 and (bytes_out < bytes_in or self.config.bigger) - ): + if replaced: return_data = self._save_new_data(final_data_buffer) if self.path_info.path: self._cleanup_filesystem(final_data_buffer) @@ -305,8 +324,11 @@ def _cleanup_after_optimize(self, final_data_buffer: BinaryIO) -> ReportStats: """Replace the old file with the better one or discard the new wasteful one.""" bytes_in = self.path_info.bytes_in() bytes_out = self._get_buffer_len(final_data_buffer) + replaced = not self.config.dry_run and ( + bytes_out > 0 and (bytes_out < bytes_in or self.config.bigger) + ) return_data = self._cleanup_after_optimize_get_return_data( - final_data_buffer, bytes_in, bytes_out + final_data_buffer, replaced=replaced ) if ( self.working_path @@ -314,19 +336,19 @@ def _cleanup_after_optimize(self, final_data_buffer: BinaryIO) -> ReportStats: and isinstance(final_data_buffer, BufferedReader) ): self.working_path.unlink(missing_ok=True) - converted = self.original_path != self.final_path + # A discarded (or dry-run) result leaves the original in place: no + # conversion happened, nothing may be renamed or stat()ed at + # final_path — for conversions it was never created. + converted = replaced and self.original_path != self.final_path if converted: self.path_info.rename(self.final_path) # For in-archive entries final_path is a synthetic name with no # filesystem reality; whether the entry "changed" is decided by # whether the worker produced replacement bytes. The parent # container's repack pass owns the real timestamp. - if self.path_info.path is None: - changed = bool(return_data) - else: - changed = self.final_path.stat().st_mtime != self._original_mtime + changed = bool(return_data) if self.path_info.path is None else replaced return ReportStats( - self.final_path, + self.final_path if replaced else self.original_path, converted=converted, path_info=self.path_info, config=self.config, diff --git a/picopt/plugins/base/tool.py b/picopt/plugins/base/tool.py index b9e99e9..ae4802e 100644 --- a/picopt/plugins/base/tool.py +++ b/picopt/plugins/base/tool.py @@ -66,6 +66,10 @@ class Tool(ABC): name: str = "" required: bool = True + # Class-level default; the first probe() sets an instance attribute. + # Tools are module singletons, so probing happens once per process — + # per-directory config rebuilds must not respawn --version subprocesses. + _probed_status: ToolStatus | None = None def parse_version(self, version: str) -> str: """Override to parse tool version specially.""" @@ -75,9 +79,16 @@ def parse_version(self, version: str) -> str: def probe_version(self) -> str: """Probe the tool version for probe().""" - @abstractmethod def probe(self) -> ToolStatus: - """Return availability, version, and path.""" + """Return availability, version, and path. Cached per instance.""" + if self._probed_status is None: + self._probed_status = self._probe() + return self._probed_status + + def _probe(self) -> ToolStatus: + """Perform the actual probe; subclasses implement.""" + msg = f"{type(self).__name__} does not implement _probe" + raise NotImplementedError(msg) def run_stage(self, handler, buf: BinaryIO) -> BinaryIO: """Transform an input buffer; default raises.""" @@ -122,7 +133,7 @@ def probe_path(self, module: ModuleType) -> str: return getattr(module, "__file__", "") or "" @override - def probe(self) -> ToolStatus: + def _probe(self) -> ToolStatus: """Return results for doctor.""" try: module = __import__(self.module_name) @@ -243,7 +254,7 @@ def probe_version(self, path: str = "") -> str: return version @override - def probe(self) -> ToolStatus: + def _probe(self) -> ToolStatus: """Doctor probe.""" path = self._path() if path is None: diff --git a/picopt/plugins/pdf.py b/picopt/plugins/pdf.py index 585ade2..e281982 100644 --- a/picopt/plugins/pdf.py +++ b/picopt/plugins/pdf.py @@ -172,8 +172,8 @@ def _is_dct_filter(filt: Any) -> bool: return False -def _has_signature(pdf: Any) -> bool: - """Return True iff the PDF has a digital signature flag set.""" +def _has_signature_flag(pdf: Any) -> bool: + """Return True iff the PDF's AcroForm signature flag is set.""" import pikepdf root = pdf.Root @@ -195,6 +195,27 @@ def _has_signature(pdf: Any) -> bool: return False +def _has_signature_object(pdf: Any) -> bool: + """ + Return True iff any object is a signature dictionary (``/Type /Sig``). + + Catches signed PDFs from non-conforming producers that never set the + AcroForm ``SigFlags`` bit; rewriting those would destroy the signature. + """ + import pikepdf + + for obj in pdf.objects: + with suppress(Exception): + if obj.get(pikepdf.Name.Type, None) == pikepdf.Name.Sig: + return True + return False + + +def _has_signature(pdf: Any) -> bool: + """Return True iff the PDF appears to be digitally signed.""" + return _has_signature_flag(pdf) or _has_signature_object(pdf) + + def _synthetic_zipinfo(objgen: tuple[int, int]) -> ZipInfo: """ Build a ZipInfo whose filename uniquely identifies a PDF stream object. @@ -387,7 +408,13 @@ def _apply_optimized_jpeg(self, child, objgen_to_obj, pikepdf) -> int: if not optimized or len(optimized) >= len(original_raw): return 0 try: - obj.write(optimized, filter=pikepdf.Name.DCTDecode) + # Preserve /DecodeParms (e.g. /ColorTransform): the optimized + # bytes are still a JPEG with the same color encoding, and + # dropping the parms can corrupt colors in some readers. + decode_parms = obj.get(pikepdf.Name.DecodeParms, None) + obj.write( + optimized, filter=pikepdf.Name.DCTDecode, decode_parms=decode_parms + ) except Exception: logger.error(f"Error writing optimized PDF image {obj}") raise @@ -431,14 +458,23 @@ def pack_into(self) -> BytesIO: try: self._apply_optimized_jpegs(pdf) output_buffer = BytesIO() - pdf.save( - output_buffer, - object_stream_mode=pikepdf.ObjectStreamMode.generate, - compress_streams=True, - stream_decode_level=pikepdf.StreamDecodeLevel.generalized, - recompress_flate=True, - linearize=False, - ) + save_kwargs: dict[str, Any] = { + "object_stream_mode": pikepdf.ObjectStreamMode.generate, + "compress_streams": True, + "recompress_flate": True, + "linearize": False, + } + if pdf.is_encrypted: + # An owner-password-restricted PDF opens without a password; + # saving without encryption=True would silently strip its + # protection. qpdf cannot decode streams while preserving + # encryption, so accept the weaker rewrite for these. + save_kwargs["encryption"] = True + else: + save_kwargs["stream_decode_level"] = ( + pikepdf.StreamDecodeLevel.generalized + ) + pdf.save(output_buffer, **save_kwargs) finally: with suppress(Exception): pdf.close() diff --git a/picopt/plugins/rar.py b/picopt/plugins/rar.py index 6569980..4e901c6 100644 --- a/picopt/plugins/rar.py +++ b/picopt/plugins/rar.py @@ -22,7 +22,6 @@ Plugin, Route, Tool, - ToolStatus, ) from picopt.plugins.base.format import FileFormat @@ -50,15 +49,11 @@ class UnrarTool(ExternalTool): def parse_version(self, version: str) -> str: """Parse unrar version.""" # this looks fragile. Tuned to unrar 7.11 beta 1. + # NOTE: unrar prints its version banner without --version (it just + # prints help). We accept that. version = super().parse_version(version) return " ".join(version.split()[1:-6]) - @override - def probe(self) -> ToolStatus: - # Default ExternalTool.probe is fine, but unrar prints its version - # banner without --version (it just prints help). We accept that. - return super().probe() - _UNRAR_TOOL = UnrarTool() diff --git a/picopt/plugins/seven_zip.py b/picopt/plugins/seven_zip.py index f5f3c04..98c195c 100644 --- a/picopt/plugins/seven_zip.py +++ b/picopt/plugins/seven_zip.py @@ -15,6 +15,7 @@ from typing import TYPE_CHECKING, Any from py7zr import SevenZipFile, is_7zfile +from py7zr.helpers import ArchiveTimestamp from py7zr.io import BytesIOFactory from typing_extensions import override @@ -108,6 +109,7 @@ def __init__( """Allocate the py7zr extraction factory.""" super().__init__(*args, **kwargs) self._factory: BytesIOFactory = BytesIOFactory(maxsize) + self._extracted: dict[str, bytes] | None = None @override @classmethod @@ -123,12 +125,25 @@ def _archive_infolist(archive): def _archive_readfile(self, archive, archiveinfo) -> bytes: if archiveinfo.is_directory: return b"" - filename = archiveinfo.filename - archive.reset() - archive.extract(targets=[filename], factory=self._factory) - if not (data := self._factory.products.get(filename)): - return b"" - return data.read() + if self._extracted is None: + # Solid 7z archives decompress from the start for every + # single-target extract — O(n^2) over members. Materialize + # everything in one pass instead; walk() reads every member's + # bytes anyway. + archive.reset() + archive.extract(factory=self._factory) + self._extracted = { + name: product.read() for name, product in self._factory.products.items() + } + return self._extracted.get(archiveinfo.filename, b"") + + @override + def _walk_finish(self) -> None: + # Don't pickle the whole archive's bytes back with the handler; + # the children already carry their own data. + self._extracted = None + self._factory = BytesIOFactory(maxsize) + super()._walk_finish() @override def _archive_for_write(self, output_buffer: BytesIO) -> SevenZipFile: @@ -140,8 +155,18 @@ def _archive_for_write(self, output_buffer: BytesIO) -> SevenZipFile: @override def _pack_info_one_file(self, archive, path_info) -> None: data = BytesIO(path_info.data()) - arcname = path_info.archiveinfo.filename() - archive.writef(data, arcname=arcname) + archiveinfo = path_info.archiveinfo + archive.writef(data, arcname=archiveinfo.filename()) + # py7zr's writef API stamps every member with now() and offers no + # override; restore the original member mtime on the header entry + # it just appended. + mtime = archiveinfo.mtime() + if mtime is not None and archive.header.files_info.files: + stamp = ArchiveTimestamp.from_datetime(mtime) + file_info = archive.header.files_info.files[-1] + file_info["creationtime"] = stamp + file_info["lastwritetime"] = stamp + file_info["lastaccesstime"] = stamp class Cb7(SevenZip): diff --git a/picopt/plugins/svg.py b/picopt/plugins/svg.py index 6392b20..292ed40 100644 --- a/picopt/plugins/svg.py +++ b/picopt/plugins/svg.py @@ -6,6 +6,8 @@ from __future__ import annotations +from pathlib import Path +from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, BinaryIO from typing_extensions import override @@ -28,15 +30,65 @@ from picopt.path import PathInfo -_SVGO_ARGS: tuple[str, ...] = ("--multipass", "--output", "-", "--input", "-") +_SVGO_ARGS: tuple[str, ...] = ("--output", "-", "--input", "-") + +# svgo's default preset is not lossless: removeViewBox breaks responsive +# scaling, and several plugins strip metadata the user asked to keep. svgo +# v2+ only accepts plugin configuration through a config file, so ship two +# explicit variants. CommonJS (.cjs) parses under both node and bun. +_SVGO_STRIP_METADATA_PLUGINS = """ + "removeMetadata", + "removeTitle", + "removeDesc", +""" +_SVGO_CONFIG_TEMPLATE = """module.exports = {{ + multipass: true, + plugins: [ + {{ + name: "preset-default", + params: {{ + overrides: {{ + removeViewBox: false, + removeMetadata: false, + removeTitle: false, + removeDesc: false, + }}, + }}, + }},{extra_plugins} + ], +}}; +""" + +_svgo_config_cache: dict[bool, Path] = {} + + +def _svgo_config_path(*, keep_metadata: bool) -> Path: + """Return a per-process temp config file for the metadata policy.""" + path = _svgo_config_cache.get(keep_metadata) + if path is None or not path.exists(): + extra_plugins = "" if keep_metadata else _SVGO_STRIP_METADATA_PLUGINS + content = _SVGO_CONFIG_TEMPLATE.format(extra_plugins=extra_plugins) + with NamedTemporaryFile( + "w", prefix="picopt_svgo_", suffix=".config.cjs", delete=False + ) as config_file: + config_file.write(content) + path = Path(config_file.name) + _svgo_config_cache[keep_metadata] = path + return path class _SvgoMixin: """Shared run_stage so the binary and the npx variants share invocation.""" - def run_stage(self, handler: Handler, buf: BinaryIO) -> BytesIO: # noqa: ARG002 + def run_stage(self, handler: Handler, buf: BinaryIO) -> BytesIO: + config_path = _svgo_config_path(keep_metadata=handler.config.keep_metadata) return Handler.run_ext( - (*self.exec_args(), *_SVGO_ARGS), # pyright:ignore[reportAttributeAccessIssue], # ty: ignore[unresolved-attribute] + ( + *self.exec_args(), # pyright:ignore[reportAttributeAccessIssue], # ty: ignore[unresolved-attribute] + "--config", + str(config_path), + *_SVGO_ARGS, + ), buf, ) diff --git a/picopt/plugins/tar.py b/picopt/plugins/tar.py index 6c4c9ec..7f3f240 100644 --- a/picopt/plugins/tar.py +++ b/picopt/plugins/tar.py @@ -118,7 +118,12 @@ def _is_archive(cls, path: Path | BytesIO) -> bool: @override def _get_archive(self) -> TarFile: - archive = tar_open(self.original_path, "r") # noqa: SIM115 + # In-archive members have no filesystem path; open from the buffer. + target = self.path_info.path_or_buffer() + if isinstance(target, BytesIO): + archive = tar_open(fileobj=target, mode="r") # noqa: SIM115 + else: + archive = tar_open(target, "r") # noqa: SIM115 if not archive: msg = f"Unknown archive type: {self.original_path}" raise ValueError(msg) @@ -131,6 +136,12 @@ def _archive_infolist(archive): @override def _archive_readfile(self, archive, archiveinfo) -> bytes: + # extractfile() on a link member returns the link TARGET's stream + # (or raises for external targets); reading it would duplicate the + # target's bytes into the link entry on repack. Links, devices, and + # fifos carry no data of their own. + if not archiveinfo.isreg(): + return b"" if buf := archive.extractfile(archiveinfo): return buf.read() return b"" @@ -150,6 +161,11 @@ def _archive_for_write(self, output_buffer: BytesIO) -> TarFile: @override def _pack_info_one_file(self, archive, path_info) -> None: tarinfo: TarInfo = path_info.archiveinfo.to_tarinfo() + if not tarinfo.isreg(): + # Symlinks, hardlinks, directories, devices: no data blocks. + # Mutating their size (or attaching data) corrupts the archive. + archive.addfile(tarinfo) + return data = path_info.data() tarinfo.size = len(data) archive.addfile(tarinfo, BytesIO(data)) diff --git a/picopt/plugins/webp.py b/picopt/plugins/webp.py deleted file mode 100644 index e962d36..0000000 --- a/picopt/plugins/webp.py +++ /dev/null @@ -1,759 +0,0 @@ -""" -WebP format plugin. - -Owns: WebP (still + animated) plus the GIF→WebP and PNG→WebP conversion -routes. Consolidates the original:: - - handlers/image/webp.py - handlers/container/animated/webp.py - handlers/container/animated/webpbase.py - handlers/container/animated/webpmux.py - handlers/container/animated/img2webp.py - config/cwebp.py - -into one file. - -The five WebP handlers cover four different unpack/pack strategies for -the same output format. They are listed in ``Route.convert`` in preference -order; the registry/config layer probes each handler's ``PIPELINE`` and -the routing layer picks the first whose tools are all available. - -- :class:`WebPLossless` still WebP. Stages: optional - PIL→PNG round-trip if input isn't - already cwebp-acceptable, then - cwebp (preferred) or PIL save. - -- :class:`Gif2WebPAnimatedLossless` animated GIF → animated WebP. - This is an :class:`ImageHandler`, - not a container — gif2webp consumes - the entire animated GIF in one shot. - -- :class:`Img2WebPAnimatedLossless` container. Frames are written to - disk as lossless WebP via Pillow at - unpack time, then ``img2webp`` packs - the frame files. - -- :class:`WebPMuxAnimatedLossless` container. ``webpmux -get frame`` - extracts frames; ``webpmux -frame`` - packs them. Only valid for animated - WebP input (webpmux can't read - anything else). - -- :class:`PILPackWebPAnimatedLossless` - container. Pure-Pillow fallback - using the inherited - :class:`ImageAnimated` walk and - ``Image.save(save_all=True)`` pack. - Empty PIPELINE — Pillow is always - available. - -The cwebp version probe (the old ``config/cwebp.py``) is folded into -:class:`CWebPTool`. On its first :meth:`probe`, the tool parses cwebp's -version and sets the class attribute :attr:`CWebPTool.IS_MODERN_CWEBP`. -:class:`WebPLossless` and :class:`Img2WebPAnimatedLossless` widen their -``_input_file_formats`` to accept PPM/TIFF directly when that flag is set, -which lets them skip the intermediate PNG round-trip. -""" - -from __future__ import annotations - -import os -import re -import shutil -import subprocess -from abc import ABC -from io import BytesIO -from itertools import zip_longest -from pathlib import Path -from tempfile import mkdtemp, mkstemp -from types import MappingProxyType -from typing import TYPE_CHECKING, Any, BinaryIO - -from loguru import logger -from PIL.WebPImagePlugin import WebPImageFile -from typing_extensions import override - -from picopt.path import PathInfo -from picopt.plugins.base import ( - ExternalTool, - Handler, - ImageAnimated, - ImageHandler, - PILSaveTool, - Plugin, - Route, - Tool, - ToolStatus, -) -from picopt.plugins.base.format import FileFormat -from picopt.plugins.gif import Gif, GifAnimated -from picopt.plugins.png import Png, PngAnimated - -# cwebp >= 1.2.3 accepts PPM and TIFF input directly. Older releases need a -# pre-conversion. CWebPTool.probe() detects the version once and stores the -# result on CWebPTool.IS_MODERN_CWEBP; handlers that benefit widen their -# accepted input formats with these entries in __init__. -_PPM_FILE_FORMAT = FileFormat("PPM", lossless=True, animated=False) -_TIFF_FILE_FORMAT = FileFormat("TIFF", lossless=True, animated=False) -MODERN_CWEBP_FORMATS: frozenset[FileFormat] = frozenset( - {_PPM_FILE_FORMAT, _TIFF_FILE_FORMAT} -) -if TYPE_CHECKING: - from collections.abc import Generator - - -_WEBP_FORMAT_STR: str = str(WebPImageFile.format) - - -# --------------------------------------------------------------------------- -# Shared helpers -# --------------------------------------------------------------------------- - - -def _run_disk_input_tool( - handler: Handler, - buf: BinaryIO, - args: tuple[str, ...], -) -> BinaryIO: - """ - Drive an external tool that needs a real input file path. - - cwebp and gif2webp don't accept stdin. If ``buf`` is already a - BufferedReader on a real file we use that file's path; otherwise we - spill the buffer to a tmp file under the working directory. - """ - is_tmp = isinstance(buf, BytesIO) - if is_tmp: - wp = handler.get_working_path() - fd, tmp_str = mkstemp(prefix=wp.name + ".", suffix=handler.output_suffix) - os.close(fd) - input_path: Path | None = Path(tmp_str) - else: - input_path = handler.path_info.path - if input_path is None: - msg = "No input path available for external WebP tool" - raise ValueError(msg) - full_args = (*args, str(input_path)) - return handler.run_ext_fs(full_args, buf, input_path, input_path_tmp=is_tmp) - - -def _hash_tmp_dir_suffix(handler: Handler) -> str: - wp = handler.get_working_path() - return f"{hash(str(wp.parent))}-{wp.name}" - - -# --------------------------------------------------------------------------- -# Tools -# --------------------------------------------------------------------------- - - -class CWebPTool(ExternalTool): - """ - The ``cwebp`` external still encoder. - - Probing it has the side effect of telling :class:`WebPLossless` whether - PPM/TIFF can be passed in directly. The doctor command will see this - tool listed under WebPLossless's PIPELINE. - """ - - name = "cwebp" - binary = "cwebp" - version_args = ("-version",) - # Set by CWebPTool.probe(). Default False so that the (rare) case of - # cwebp being missing entirely doesn't accidentally widen the input - # format set. - # cwebp before this version only accepts PNG and WebP as input formats. - # Newer cwebps accept PPM and TIFF as well, which lets us skip the - # intermediate PNG round-trip when picopt is converting from those. - _MIN_CWEBP_VERSION: tuple[int, int, int] = (1, 2, 3) - IS_MODERN_CWEBP: bool = False - - @staticmethod - def _parse_cwebp_version(version_str: str) -> tuple[int, ...]: - """Parse '1.3.2' or '1.3.2 libwebp 1.3.2' into a tuple of ints.""" - if not version_str: - return () - head = version_str.split(maxsplit=1)[0] - parts: list[int] = [] - for part in head.split("."): - try: - parts.append(int(part)) - except ValueError: - break - return tuple(parts) - - @override - def probe(self) -> ToolStatus: - status = super().probe() - if status.available: - parsed = CWebPTool._parse_cwebp_version(status.version) - # Set on class - CWebPTool.IS_MODERN_CWEBP = ( - bool(parsed) and parsed >= CWebPTool._MIN_CWEBP_VERSION - ) - return status - - @override - def run_stage(self, handler: Handler, buf: BinaryIO) -> BinaryIO: - if not isinstance(handler, WebPLossless): - msg = f"CWebPTool cannot run on {type(handler).__name__}" - raise TypeError(msg) - args = (*self.exec_args(), *handler.cwebp_args()) - return _run_disk_input_tool(handler, buf, args) - - -class WebPExternalTool(ExternalTool, ABC): - """WebP Tool Suite Tool.""" - - version_args = ("-version",) - - @override - def parse_version(self, version: str) -> str: - """Version is last term.""" - version = super().parse_version(version) - return version.split()[-1] - - -class Gif2WebPTool(WebPExternalTool): - """The ``gif2webp`` external animated-GIF encoder.""" - - name = "gif2webp" - binary = "gif2webp" - - @override - def run_stage(self, handler: Handler, buf: BinaryIO) -> BinaryIO: - if not isinstance(handler, Gif2WebPAnimatedLossless): - msg = f"Gif2WebPTool cannot run on {type(handler).__name__}" - raise TypeError(msg) - args = (*self.exec_args(), *handler.gif2webp_args()) - return _run_disk_input_tool(handler, buf, args) - - -class Img2WebPTool(WebPExternalTool): - """ - The ``img2webp`` external animated-WebP packer. - - Implements ``run_pack`` rather than ``run_stage`` because img2webp's - role is to assemble already-extracted frame files into the final - animated WebP. The handler that owns this tool extracts frames to - disk during ``walk()``; ``run_pack()`` reads the frame paths off the - handler and shells out. - """ - - name = "img2webp" - binary = "img2webp" - - @override - def run_pack(self, handler: Handler) -> BinaryIO: - if not isinstance(handler, Img2WebPAnimatedLossless): - msg = f"Img2WebPTool cannot pack {type(handler).__name__}" - raise TypeError(msg) - args = (*self.exec_args(), *handler.img2webp_args()) - proc = subprocess.run(args, check=True, capture_output=True) # noqa: S603 - return BytesIO(proc.stdout) - - -class WebPMuxTool(ExternalTool): - """ - The ``webpmux`` external animated-WebP demuxer + packer. - - Used for both unpacking (``webpmux -get frame N``) and packing - (``webpmux -frame ... -loop 0``). The unpack side is invoked from the - handler's overridden ``walk()``; the pack side is the standard - ``run_pack`` entry point that the handler's ``pack_into`` calls. - """ - - name = "webpmux" - binary = "webpmux" - version_args = ("-version",) - - @override - def run_pack(self, handler: Handler) -> BinaryIO: - if not isinstance(handler, WebPMuxAnimatedLossless): - msg = f"WebPMuxTool cannot pack {type(handler).__name__}" - raise TypeError(msg) - args = (*self.exec_args(), *handler.webpmux_pack_args()) - proc = subprocess.run(args, check=True, capture_output=True) # noqa: S603 - return BytesIO(proc.stdout) - - -# --------------------------------------------------------------------------- -# Still WebP -# --------------------------------------------------------------------------- - - -class WebPLossless(ImageHandler): - """Lossless still WebP handler.""" - - OUTPUT_FORMAT_STR = _WEBP_FORMAT_STR - OUTPUT_FILE_FORMAT = FileFormat(_WEBP_FORMAT_STR, lossless=True, animated=False) - INPUT_FILE_FORMATS = frozenset({OUTPUT_FILE_FORMAT, Png.OUTPUT_FILE_FORMAT}) - SUFFIXES: tuple[str, ...] = (".webp",) - - PIL2_KWARGS: MappingProxyType[str, Any] = MappingProxyType( - {"quality": 100, "method": 6, "lossless": True} - ) - - # Tier 1: convert non-acceptable inputs to PNG via Pillow. If the input - # is already in INPUT_FILE_FORMATS this is a no-op (pil_save - # short-circuits when self.input_file_format is acceptable). - # Tier 2: encode to WebP. cwebp wins when available; otherwise Pillow - # saves directly to WebP. - PIPELINE: tuple[tuple[Tool, ...], ...] = ( - ( - PILSaveTool( - target_format_str="PNG", - save_kwargs={"compress_level": 0}, - name="pil2png", - ), - ), - ( - CWebPTool(), - PILSaveTool(target_format_str=_WEBP_FORMAT_STR, name="pil2webp"), - ), - ) - - # https://developers.google.com/speed/webp/docs/cwebp - _CWEBP_BASE_ARGS: tuple[str, ...] = ( - "-mt", - "-q", - "100", - "-m", - "6", - "-o", - "-", - "-quiet", - # https://groups.google.com/a/webmproject.org/g/webp-discuss/c/0GmxDmlexek - "-lossless", - "-sharp_yuv", - "-alpha_filter", - "best", - ) - _NEAR_LOSSLESS_ARGS: tuple[str, ...] = ("-near_lossless", "0") - - def __init__( - self, - *args: Any, - **kwargs: Any, - ) -> None: - """Widen acceptable inputs if cwebp is modern enough for PPM/TIFF.""" - super().__init__(*args, **kwargs) - if CWebPTool.IS_MODERN_CWEBP: - self._input_file_formats = self._input_file_formats | MODERN_CWEBP_FORMATS - - def cwebp_args(self) -> tuple[str, ...]: - """Build the runtime cwebp argument tuple (no exec / input path).""" - meta = ("all",) if self.config.keep_metadata else ("none",) - args: tuple[str, ...] = (*self._CWEBP_BASE_ARGS, "-metadata", *meta) - if self.config.near_lossless: - args = (*args, *self._NEAR_LOSSLESS_ARGS) - return args - - -# --------------------------------------------------------------------------- -# Animated GIF → animated WebP (image handler, not container) -# --------------------------------------------------------------------------- - - -class Gif2WebPAnimatedLossless(ImageHandler): - """ - Convert animated GIF to animated WebP via gif2webp. - - This is intentionally an :class:`ImageHandler` rather than a container: - gif2webp takes the entire input GIF as a single argument and writes a - single WebP. There are no frames to walk on the picopt side. - """ - - OUTPUT_FORMAT_STR = _WEBP_FORMAT_STR - OUTPUT_FILE_FORMAT = FileFormat(_WEBP_FORMAT_STR, lossless=True, animated=True) - INPUT_FILE_FORMATS = frozenset({GifAnimated.OUTPUT_FILE_FORMAT}) - SUFFIXES: tuple[str, ...] = (".webp",) - - PIPELINE: tuple[tuple[Tool, ...], ...] = ((Gif2WebPTool(),),) - - PIL2_KWARGS: MappingProxyType[str, Any] = MappingProxyType( - {"quality": 100, "method": 6, "lossless": True} - ) - - # https://developers.google.com/speed/webp/docs/gif2webp - _GIF2WEBP_BASE_ARGS: tuple[str, ...] = ( - "-mt", - "-q", - "100", - "-m", - "6", - "-o", - "-", - "-quiet", - "-min_size", - ) - - def gif2webp_args(self) -> tuple[str, ...]: - """Args for extrnal program.""" - meta = ("all",) if self.config.keep_metadata else ("none",) - return (*self._GIF2WEBP_BASE_ARGS, "-metadata", *meta) - - -# --------------------------------------------------------------------------- -# Animated WebP — container handlers -# --------------------------------------------------------------------------- - -# Common PIL save options shared by every animated-WebP handler. -_ANIMATED_WEBP_PIL_KWARGS: MappingProxyType[str, Any] = MappingProxyType( - {"quality": 100, "method": 6, "lossless": True, "minimize_size": True} -) - -# Frame-extraction PIL options when frames need to be written as -# intermediate WebP files (img2webp / PIL fallback). -_ANIMATED_WEBP_FRAME_KWARGS: MappingProxyType[str, Any] = MappingProxyType( - { - "format": _WEBP_FORMAT_STR, - "method": 0, - "lossless": True, - "quality": 0, - } -) - - -class WebPAnimatedLossless(ImageAnimated, ABC): - """Common WebPAnimated Tool methods.""" - - OUTPUT_FORMAT_STR = _WEBP_FORMAT_STR - OUTPUT_FILE_FORMAT = FileFormat(_WEBP_FORMAT_STR, lossless=True, animated=True) - SUFFIXES: tuple[str, ...] = (".webp",) - PIL2_KWARGS: MappingProxyType[str, Any] = _ANIMATED_WEBP_PIL_KWARGS - PIL2_FRAME_KWARGS: MappingProxyType[str, Any] = _ANIMATED_WEBP_FRAME_KWARGS - - def __init__( - self, - *args: Any, - **kwargs: Any, - ) -> None: - """Initialize instance.""" - super().__init__(*args, **kwargs) - self._working_tmp_dir: Path | None = None - self._frame_index_width: int = 0 - - # ----- frame file plumbing - def _ensure_tmp_dir(self) -> Path: - if self._working_tmp_dir is None: - self._working_tmp_dir = Path(mkdtemp(suffix=_hash_tmp_dir_suffix(self))) - return self._working_tmp_dir - - def _frame_path(self, index: int) -> Path: - n = str(index).zfill(self._frame_index_width) - return self._ensure_tmp_dir() / f"frame_{n}.webp" - - # ----- pack - # - def _cleanup_tmp_dir(self) -> None: - if self._working_tmp_dir is not None: - shutil.rmtree(self._working_tmp_dir, ignore_errors=True) - self._working_tmp_dir = None - - -class Img2WebPAnimatedLossless(WebPAnimatedLossless): - """ - Animated WebP packed via ``img2webp``. - - During :meth:`walk` (inherited), Pillow's frame iterator yields frames - one by one and our overridden :meth:`_unpack_frame` saves each one as - a lossless WebP file inside a per-handler tmp directory. At pack time, - :class:`Img2WebPTool` shells out with one ``-frame`` argument per - saved file. The tmp dir is removed in a ``finally`` from - :meth:`pack_into` regardless of success. - """ - - INPUT_FILE_FORMATS = frozenset( - {WebPAnimatedLossless.OUTPUT_FILE_FORMAT, *PngAnimated.INPUT_FILE_FORMATS} - ) - - PIPELINE: tuple[tuple[Tool, ...], ...] = ((Img2WebPTool(),),) - - # https://developers.google.com/speed/webp/docs/img2webp - _IMG2WEBP_BASE_ARGS: tuple[str, ...] = ( - "-min_size", - "-q", - "100", - "-m", - "6", - "-o", - "-", - "-sharp_yuv", - ) - _LOSSLESS_OPTS: tuple[str, ...] = ("-lossless",) - _NEAR_LOSSLESS_OPTS: tuple[str, ...] = ("-near_lossless", "0") - - def __init__( - self, - *args: Any, - **kwargs: Any, - ) -> None: - """Init instance variables.""" - super().__init__(*args, **kwargs) - if CWebPTool.IS_MODERN_CWEBP: - self._input_file_formats = self._input_file_formats | MODERN_CWEBP_FORMATS - self._frame_paths: list[Path] = [] - - # ----- walk overrides - - @override - def _unpack_frame( - self, - frame, - frame_index: int, - frame_info: dict, - ) -> PathInfo: - self.populate_frame_info(frame, frame_info) - path = self._frame_path(frame_index) - frame.save(path, **self.PIL2_FRAME_KWARGS) - self._frame_paths.append(path) - return PathInfo( - path_info=self.path_info, - frame=frame_index, - container_parents=self.path_info.container_path_history(), - noop=True, - ) - - @override - def walk(self) -> Generator[PathInfo]: - # img2webp owns the entire packing strategy; we always repack. - self._frame_index_width = len(str(self.info.get("n_frames", 1))) - self._ensure_tmp_dir() - yield from super().walk() - self._do_repack = True - - # ----- pack - - @override - def pack_into(self) -> BinaryIO: - try: - return self.first_stage().run_pack(self) - finally: - self._cleanup_tmp_dir() - - def img2webp_args(self) -> tuple[str, ...]: - """Args for external program.""" - runtime: tuple[str, ...] = ( - self._NEAR_LOSSLESS_OPTS - if self.config.near_lossless - else self._LOSSLESS_OPTS - ) - if loop := self.frame_info.get("loop"): - runtime = (*runtime, "-loop", str(loop)) - - out: list[str] = [*self._IMG2WEBP_BASE_ARGS, *runtime] - durations = self.frame_info.get("duration", ()) - for frame_duration, frame_path in zip_longest( - durations, sorted(self._frame_paths), fillvalue=None - ): - if frame_path is None: - continue - if frame_duration is not None: - out += ["-d", str(frame_duration)] - out.append(str(frame_path)) - return tuple(out) - - -class WebPMuxAnimatedLossless(WebPAnimatedLossless): - """ - Animated WebP unpacked and repacked via ``webpmux``. - - Only meaningful when the input is already animated WebP — webpmux - can't read anything else. Both unpack and pack go through the same - binary; we override :meth:`walk` entirely (replacing Pillow's frame - iterator) and :meth:`pack_into` calls :class:`WebPMuxTool`. - """ - - INPUT_FILE_FORMATS = frozenset({WebPAnimatedLossless.OUTPUT_FILE_FORMAT}) - - PIPELINE: tuple[tuple[Tool, ...], ...] = ((WebPMuxTool(),),) - - _DURATION_RE: re.Pattern[str] = re.compile(r"frame \d+: (\d+) ms") - _DEFAULT_DURATION_MS: int = 100 - - def __init__( - self, - *args: Any, - **kwargs: Any, - ) -> None: - """Initialize instance.""" - super().__init__(*args, **kwargs) - self._durations: dict[int, int] = {} - - # ----- frame file plumbing - def _webpmux_exec(self) -> tuple[str, ...]: - """Resolve ``webpmux`` argv prefix from the probed tool instance.""" - return self.resolved_tool(WebPMuxTool).exec_args() - - def _read_durations(self, num_frames: int) -> dict[int, int]: - cmd = (*self._webpmux_exec(), "-info", str(self.original_path)) - result = subprocess.run(cmd, check=True, capture_output=True) # noqa: S603 - text = result.stdout.decode("utf-8", errors="replace") - durations = self._DURATION_RE.findall(text) - if durations: - return {i: int(d) for i, d in enumerate(durations, start=1)} - return dict.fromkeys(range(1, num_frames + 1), self._DEFAULT_DURATION_MS) - - # ----- walk - - @override - def walk(self) -> Generator[PathInfo]: - if self.config.verbose > 1: - logger.info(f"Unpacking {self.path_info.full_output_name()}…") - n_frames = self.info["n_frames"] - self._frame_index_width = len(str(n_frames)) - self._ensure_tmp_dir() - - webpmux = self._webpmux_exec() - container_parents = self.path_info.container_path_history() - extracted: list[Path] = [] - for frame_index in range(1, n_frames + 1): - frame_path = self._frame_path(frame_index) - cmd = ( - *webpmux, - "-get", - "frame", - str(frame_index), - str(self.original_path), - "-o", - str(frame_path), - ) - try: - subprocess.run(cmd, check=True, capture_output=True) # noqa: S603 - except subprocess.CalledProcessError: - break - yield PathInfo( - path_info=self.path_info, - path=frame_path, - frame=frame_index, - container_parents=container_parents, - ) - if frame_path.exists(): - extracted.append(frame_path) - - if not extracted: - msg = "No frames found — is this actually an animated WebP?" - raise ValueError(msg) - - self._durations = self._read_durations(len(extracted)) - self._do_repack = True - self._walk_finish() - - # ----- pack - - @override - def pack_into(self) -> BinaryIO: - try: - return self.first_stage().run_pack(self) - finally: - self._cleanup_tmp_dir() - - def webpmux_pack_args(self) -> tuple[str, ...]: - """Args for external tool.""" - out: list[str] = [] - for index, dur in self._durations.items(): - out.extend(["-frame", str(self._frame_path(index)), f"+{dur}"]) - out.extend(["-loop", "0", "-o", "-"]) - return tuple(out) - - -class PILPackWebPAnimatedLossless(ImageAnimated): - """ - Pure-Pillow animated WebP fallback. - - The inherited :meth:`ImageAnimated.walk` extracts frames in memory and - the inherited :meth:`ImageAnimated.pack_into` calls - ``Image.save(save_all=True)`` with this class's PIL kwargs. Pillow is - a hard picopt dependency, so this handler is always available. - - PIPELINE contains a single :class:`PILSaveTool` sentinel so the doctor - command lists this handler with a real tool entry instead of a blank - "no stages" line. The sentinel is informational only — - :meth:`ContainerHandler.optimize` calls :meth:`pack_into` directly and - never iterates the pipeline as stages. - """ - - OUTPUT_FORMAT_STR = _WEBP_FORMAT_STR - OUTPUT_FILE_FORMAT = FileFormat(_WEBP_FORMAT_STR, lossless=True, animated=True) - INPUT_FILE_FORMATS = frozenset( - {OUTPUT_FILE_FORMAT, *PngAnimated.INPUT_FILE_FORMATS} - ) - SUFFIXES: tuple[str, ...] = (".webp",) - PIPELINE: tuple[tuple[Tool, ...], ...] = ( - (PILSaveTool(target_format_str=_WEBP_FORMAT_STR, name="pil2webp"),), - ) - - PIL2_KWARGS: MappingProxyType[str, Any] = _ANIMATED_WEBP_PIL_KWARGS - PIL2_FRAME_KWARGS: MappingProxyType[str, Any] = _ANIMATED_WEBP_FRAME_KWARGS - - -# --------------------------------------------------------------------------- -# Plugin descriptor -# --------------------------------------------------------------------------- - -# Convert-target preference for animated-WebP outputs from non-WebP -# inputs (animated GIF, animated PNG). webpmux is intentionally absent — -# it can only read existing animated WebP. img2webp is preferred for -# size; PIL is the always-available fallback. -_ANIMATED_CONVERT_FROM_OTHER: tuple[type[Handler], ...] = ( - Img2WebPAnimatedLossless, - PILPackWebPAnimatedLossless, -) - - -PLUGIN = Plugin( - name="WEBP", - handlers=( - WebPLossless, - Gif2WebPAnimatedLossless, - Img2WebPAnimatedLossless, - WebPMuxAnimatedLossless, - PILPackWebPAnimatedLossless, - ), - routes=( - # Still WebP → still WebPLossless. - Route( - file_format=WebPLossless.OUTPUT_FILE_FORMAT, - native=WebPLossless, - ), - Route( - file_format=Png.OUTPUT_FILE_FORMAT, - convert=(WebPLossless,), - ), - Route( - file_format=Gif.OUTPUT_FILE_FORMAT, - convert=(WebPLossless,), - ), - # Animated WebP input: webpmux is the natural native handler - # because it can round-trip without re-encoding via Pillow. - # Img2Webp / PILPack are listed as convert alternatives so the - # routing layer can fall back if webpmux is missing. - Route( - file_format=WebPMuxAnimatedLossless.OUTPUT_FILE_FORMAT, - native=WebPMuxAnimatedLossless, - convert=_ANIMATED_CONVERT_FROM_OTHER, - ), - # Convert from animated GIF: prefer gif2webp (purpose-built); - # fall back to img2webp / PIL via PNG-frame extraction. - Route( - file_format=GifAnimated.OUTPUT_FILE_FORMAT, - convert=( - Gif2WebPAnimatedLossless, - *_ANIMATED_CONVERT_FROM_OTHER, - ), - ), - # Convert from animated PNG: img2webp / PIL - # doesn't read PNG. - Route( - file_format=PngAnimated.OUTPUT_FILE_FORMAT, - convert=_ANIMATED_CONVERT_FROM_OTHER, - ), - ), - convert_targets=( - WebPLossless, - Img2WebPAnimatedLossless, - PILPackWebPAnimatedLossless, - ), - default_enabled=True, -) diff --git a/picopt/plugins/webp/__init__.py b/picopt/plugins/webp/__init__.py new file mode 100644 index 0000000..47189c2 --- /dev/null +++ b/picopt/plugins/webp/__init__.py @@ -0,0 +1,133 @@ +""" +WebP format plugin. + +Owns: WebP (still + animated) plus the GIF→WebP and PNG→WebP conversion +routes, split across: + +- :mod:`picopt.plugins.webp.tools` — the external cwebp/gif2webp/ + img2webp/webpmux tools and the probed :data:`CWEBP_TOOL` singleton. +- :mod:`picopt.plugins.webp.static` — still :class:`WebPLossless`. +- :mod:`picopt.plugins.webp.animated` — the four animated handlers. + +The five WebP handlers cover four different unpack/pack strategies for +the same output format. They are listed in ``Route.convert`` in preference +order; the registry/config layer probes each handler's ``PIPELINE`` and +the routing layer picks the first whose tools are all available. + +- :class:`WebPLossless` still WebP. Stages: optional + PIL→PNG round-trip if input isn't + already cwebp-acceptable, then + cwebp (preferred) or PIL save. + +- :class:`Gif2WebPAnimatedLossless` animated GIF → animated WebP in one + gif2webp invocation (an ImageHandler, + not a container). + +- :class:`Img2WebPAnimatedLossless` container. Frames are written to + disk as lossless WebP via Pillow at + unpack time, then ``img2webp`` packs + the frame files. + +- :class:`WebPMuxAnimatedLossless` container. ``webpmux -get frame`` + extracts frames; ``webpmux -frame`` + packs them. Only valid for animated + WebP input. + +- :class:`PILPackWebPAnimatedLossless` + container. Pure-Pillow fallback; + always available. +""" + +from picopt.plugins.base import Handler, Plugin, Route +from picopt.plugins.gif import Gif, GifAnimated +from picopt.plugins.png import Png, PngAnimated +from picopt.plugins.webp.animated import ( + Gif2WebPAnimatedLossless, + Img2WebPAnimatedLossless, + PILPackWebPAnimatedLossless, + WebPAnimatedLossless, + WebPMuxAnimatedLossless, +) +from picopt.plugins.webp.const import MODERN_CWEBP_FORMATS, WEBP_FORMAT_STR +from picopt.plugins.webp.static import WebPLossless +from picopt.plugins.webp.tools import CWEBP_TOOL, CWebPTool + +__all__ = ( + "CWEBP_TOOL", + "MODERN_CWEBP_FORMATS", + "PLUGIN", + "WEBP_FORMAT_STR", + "CWebPTool", + "Gif2WebPAnimatedLossless", + "Img2WebPAnimatedLossless", + "PILPackWebPAnimatedLossless", + "WebPAnimatedLossless", + "WebPLossless", + "WebPMuxAnimatedLossless", +) + +# Convert-target preference for animated-WebP outputs from non-WebP +# inputs (animated GIF, animated PNG). webpmux is intentionally absent — +# it can only read existing animated WebP. img2webp is preferred for +# size; PIL is the always-available fallback. +_ANIMATED_CONVERT_FROM_OTHER: tuple[type[Handler], ...] = ( + Img2WebPAnimatedLossless, + PILPackWebPAnimatedLossless, +) + + +PLUGIN = Plugin( + name="WEBP", + handlers=( + WebPLossless, + Gif2WebPAnimatedLossless, + Img2WebPAnimatedLossless, + WebPMuxAnimatedLossless, + PILPackWebPAnimatedLossless, + ), + routes=( + # Still WebP → still WebPLossless. + Route( + file_format=WebPLossless.OUTPUT_FILE_FORMAT, + native=WebPLossless, + ), + Route( + file_format=Png.OUTPUT_FILE_FORMAT, + convert=(WebPLossless,), + ), + Route( + file_format=Gif.OUTPUT_FILE_FORMAT, + convert=(WebPLossless,), + ), + # Animated WebP input: webpmux is the natural native handler + # because it can round-trip without re-encoding via Pillow. + # Img2Webp / PILPack are listed as convert alternatives so the + # routing layer can fall back if webpmux is missing. + Route( + file_format=WebPMuxAnimatedLossless.OUTPUT_FILE_FORMAT, + native=WebPMuxAnimatedLossless, + convert=_ANIMATED_CONVERT_FROM_OTHER, + ), + # Convert from animated GIF: prefer gif2webp (purpose-built); + # fall back to img2webp / PIL via PNG-frame extraction. + Route( + file_format=GifAnimated.OUTPUT_FILE_FORMAT, + convert=( + Gif2WebPAnimatedLossless, + *_ANIMATED_CONVERT_FROM_OTHER, + ), + ), + # Convert from animated PNG: img2webp / PIL + # doesn't read PNG. + Route( + file_format=PngAnimated.OUTPUT_FILE_FORMAT, + convert=_ANIMATED_CONVERT_FROM_OTHER, + ), + ), + convert_targets=( + WebPLossless, + Img2WebPAnimatedLossless, + PILPackWebPAnimatedLossless, + ), + default_enabled=True, +) diff --git a/picopt/plugins/webp/animated.py b/picopt/plugins/webp/animated.py new file mode 100644 index 0000000..7db81f3 --- /dev/null +++ b/picopt/plugins/webp/animated.py @@ -0,0 +1,367 @@ +"""Animated WebP handlers: gif2webp, img2webp, webpmux, and PIL fallback.""" + +from __future__ import annotations + +import re +import shutil +import subprocess +from abc import ABC +from itertools import zip_longest +from pathlib import Path +from tempfile import mkdtemp +from types import MappingProxyType +from typing import TYPE_CHECKING, Any, BinaryIO + +from loguru import logger +from typing_extensions import override + +from picopt.path import PathInfo +from picopt.plugins.base import ImageAnimated, ImageHandler, PILSaveTool, Tool +from picopt.plugins.base.format import FileFormat +from picopt.plugins.gif import GifAnimated +from picopt.plugins.png import PngAnimated +from picopt.plugins.webp.const import MODERN_CWEBP_FORMATS, WEBP_FORMAT_STR +from picopt.plugins.webp.tools import ( + CWEBP_TOOL, + Gif2WebPTool, + Img2WebPTool, + WebPMuxTool, + hash_tmp_dir_suffix, +) + +if TYPE_CHECKING: + from collections.abc import Generator + + +class Gif2WebPAnimatedLossless(ImageHandler): + """ + Convert animated GIF to animated WebP via gif2webp. + + This is intentionally an :class:`ImageHandler` rather than a container: + gif2webp takes the entire input GIF as a single argument and writes a + single WebP. There are no frames to walk on the picopt side. + """ + + OUTPUT_FORMAT_STR = WEBP_FORMAT_STR + OUTPUT_FILE_FORMAT = FileFormat(WEBP_FORMAT_STR, lossless=True, animated=True) + INPUT_FILE_FORMATS = frozenset({GifAnimated.OUTPUT_FILE_FORMAT}) + SUFFIXES: tuple[str, ...] = (".webp",) + + PIPELINE: tuple[tuple[Tool, ...], ...] = ((Gif2WebPTool(),),) + + PIL2_KWARGS: MappingProxyType[str, Any] = MappingProxyType( + {"quality": 100, "method": 6, "lossless": True} + ) + + # https://developers.google.com/speed/webp/docs/gif2webp + _GIF2WEBP_BASE_ARGS: tuple[str, ...] = ( + "-mt", + "-q", + "100", + "-m", + "6", + "-o", + "-", + "-quiet", + "-min_size", + ) + + def gif2webp_args(self) -> tuple[str, ...]: + """Args for extrnal program.""" + meta = ("all",) if self.config.keep_metadata else ("none",) + return (*self._GIF2WEBP_BASE_ARGS, "-metadata", *meta) + + +# Common PIL save options shared by every animated-WebP handler. +_ANIMATED_WEBP_PIL_KWARGS: MappingProxyType[str, Any] = MappingProxyType( + {"quality": 100, "method": 6, "lossless": True, "minimize_size": True} +) + +# Frame-extraction PIL options when frames need to be written as +# intermediate WebP files (img2webp / PIL fallback). +_ANIMATED_WEBP_FRAME_KWARGS: MappingProxyType[str, Any] = MappingProxyType( + { + "format": WEBP_FORMAT_STR, + "method": 0, + "lossless": True, + "quality": 0, + } +) + + +class WebPAnimatedLossless(ImageAnimated, ABC): + """Common WebPAnimated Tool methods.""" + + OUTPUT_FORMAT_STR = WEBP_FORMAT_STR + OUTPUT_FILE_FORMAT = FileFormat(WEBP_FORMAT_STR, lossless=True, animated=True) + SUFFIXES: tuple[str, ...] = (".webp",) + PIL2_KWARGS: MappingProxyType[str, Any] = _ANIMATED_WEBP_PIL_KWARGS + PIL2_FRAME_KWARGS: MappingProxyType[str, Any] = _ANIMATED_WEBP_FRAME_KWARGS + + def __init__( + self, + *args: Any, + **kwargs: Any, + ) -> None: + """Initialize instance.""" + super().__init__(*args, **kwargs) + self._working_tmp_dir: Path | None = None + self._frame_index_width: int = 0 + + # ----- frame file plumbing + def _ensure_tmp_dir(self) -> Path: + if self._working_tmp_dir is None: + self._working_tmp_dir = Path(mkdtemp(suffix=hash_tmp_dir_suffix(self))) + return self._working_tmp_dir + + def _frame_path(self, index: int) -> Path: + n = str(index).zfill(self._frame_index_width) + return self._ensure_tmp_dir() / f"frame_{n}.webp" + + # ----- pack + # + def _cleanup_tmp_dir(self) -> None: + if self._working_tmp_dir is not None: + shutil.rmtree(self._working_tmp_dir, ignore_errors=True) + self._working_tmp_dir = None + + @override + def get_staging_dir(self) -> Path | None: + """Expose the frame tmp dir so the scheduler can clean it on rollback.""" + return self._working_tmp_dir + + +class Img2WebPAnimatedLossless(WebPAnimatedLossless): + """ + Animated WebP packed via ``img2webp``. + + During :meth:`walk` (inherited), Pillow's frame iterator yields frames + one by one and our overridden :meth:`_unpack_frame` saves each one as + a lossless WebP file inside a per-handler tmp directory. At pack time, + :class:`Img2WebPTool` shells out with one ``-frame`` argument per + saved file. The tmp dir is removed in a ``finally`` from + :meth:`pack_into` regardless of success. + """ + + INPUT_FILE_FORMATS = frozenset( + {WebPAnimatedLossless.OUTPUT_FILE_FORMAT, *PngAnimated.INPUT_FILE_FORMATS} + ) + + PIPELINE: tuple[tuple[Tool, ...], ...] = ((Img2WebPTool(),),) + + # https://developers.google.com/speed/webp/docs/img2webp + _IMG2WEBP_BASE_ARGS: tuple[str, ...] = ( + "-min_size", + "-q", + "100", + "-m", + "6", + "-o", + "-", + "-sharp_yuv", + ) + _LOSSLESS_OPTS: tuple[str, ...] = ("-lossless",) + _NEAR_LOSSLESS_OPTS: tuple[str, ...] = ("-near_lossless", "0") + + def __init__( + self, + *args: Any, + **kwargs: Any, + ) -> None: + """Init instance variables.""" + super().__init__(*args, **kwargs) + if CWEBP_TOOL.is_modern: + self._input_file_formats = self._input_file_formats | MODERN_CWEBP_FORMATS + self._frame_paths: list[Path] = [] + + # ----- walk overrides + + @override + def _unpack_frame( + self, + frame, + frame_index: int, + frame_info: dict, + ) -> PathInfo: + self.populate_frame_info(frame, frame_info) + path = self._frame_path(frame_index) + frame.save(path, **self.PIL2_FRAME_KWARGS) + self._frame_paths.append(path) + return PathInfo( + path_info=self.path_info, + frame=frame_index, + container_parents=self.path_info.container_path_history(), + noop=True, + ) + + @override + def walk(self) -> Generator[PathInfo]: + # img2webp owns the entire packing strategy; we always repack. + self._frame_index_width = len(str(self.info.get("n_frames", 1))) + self._ensure_tmp_dir() + yield from super().walk() + self._do_repack = True + + # ----- pack + + @override + def pack_into(self) -> BinaryIO: + try: + return self.first_stage().run_pack(self) + finally: + self._cleanup_tmp_dir() + + def img2webp_args(self) -> tuple[str, ...]: + """Args for external program.""" + runtime: tuple[str, ...] = ( + self._NEAR_LOSSLESS_OPTS + if self.config.near_lossless + else self._LOSSLESS_OPTS + ) + if loop := self.frame_info.get("loop"): + runtime = (*runtime, "-loop", str(loop)) + + out: list[str] = [*self._IMG2WEBP_BASE_ARGS, *runtime] + durations = self.frame_info.get("duration", ()) + for frame_duration, frame_path in zip_longest( + durations, sorted(self._frame_paths), fillvalue=None + ): + if frame_path is None: + continue + if frame_duration is not None: + out += ["-d", str(frame_duration)] + out.append(str(frame_path)) + return tuple(out) + + +class WebPMuxAnimatedLossless(WebPAnimatedLossless): + """ + Animated WebP unpacked and repacked via ``webpmux``. + + Only meaningful when the input is already animated WebP — webpmux + can't read anything else. Both unpack and pack go through the same + binary; we override :meth:`walk` entirely (replacing Pillow's frame + iterator) and :meth:`pack_into` calls :class:`WebPMuxTool`. + """ + + INPUT_FILE_FORMATS = frozenset({WebPAnimatedLossless.OUTPUT_FILE_FORMAT}) + + PIPELINE: tuple[tuple[Tool, ...], ...] = ((WebPMuxTool(),),) + + _DURATION_RE: re.Pattern[str] = re.compile(r"frame \d+: (\d+) ms") + _DEFAULT_DURATION_MS: int = 100 + + def __init__( + self, + *args: Any, + **kwargs: Any, + ) -> None: + """Initialize instance.""" + super().__init__(*args, **kwargs) + self._durations: dict[int, int] = {} + + # ----- frame file plumbing + def _webpmux_exec(self) -> tuple[str, ...]: + """Resolve ``webpmux`` argv prefix from the probed tool instance.""" + return self.resolved_tool(WebPMuxTool).exec_args() + + def _read_durations(self, num_frames: int) -> dict[int, int]: + cmd = (*self._webpmux_exec(), "-info", str(self.original_path)) + result = subprocess.run(cmd, check=True, capture_output=True) # noqa: S603 + text = result.stdout.decode("utf-8", errors="replace") + durations = self._DURATION_RE.findall(text) + if durations: + return {i: int(d) for i, d in enumerate(durations, start=1)} + return dict.fromkeys(range(1, num_frames + 1), self._DEFAULT_DURATION_MS) + + # ----- walk + + @override + def walk(self) -> Generator[PathInfo]: + if self.config.verbose > 1: + logger.info(f"Unpacking {self.path_info.full_output_name()}…") + n_frames = self.info["n_frames"] + self._frame_index_width = len(str(n_frames)) + self._ensure_tmp_dir() + + webpmux = self._webpmux_exec() + container_parents = self.path_info.container_path_history() + extracted: list[Path] = [] + for frame_index in range(1, n_frames + 1): + frame_path = self._frame_path(frame_index) + cmd = ( + *webpmux, + "-get", + "frame", + str(frame_index), + str(self.original_path), + "-o", + str(frame_path), + ) + try: + subprocess.run(cmd, check=True, capture_output=True) # noqa: S603 + except subprocess.CalledProcessError: + break + yield PathInfo( + path_info=self.path_info, + path=frame_path, + frame=frame_index, + container_parents=container_parents, + ) + if frame_path.exists(): + extracted.append(frame_path) + + if not extracted: + msg = "No frames found — is this actually an animated WebP?" + raise ValueError(msg) + + self._durations = self._read_durations(len(extracted)) + self._do_repack = True + self._walk_finish() + + # ----- pack + + @override + def pack_into(self) -> BinaryIO: + try: + return self.first_stage().run_pack(self) + finally: + self._cleanup_tmp_dir() + + def webpmux_pack_args(self) -> tuple[str, ...]: + """Args for external tool.""" + out: list[str] = [] + for index, dur in self._durations.items(): + out.extend(["-frame", str(self._frame_path(index)), f"+{dur}"]) + out.extend(["-loop", "0", "-o", "-"]) + return tuple(out) + + +class PILPackWebPAnimatedLossless(ImageAnimated): + """ + Pure-Pillow animated WebP fallback. + + The inherited :meth:`ImageAnimated.walk` extracts frames in memory and + the inherited :meth:`ImageAnimated.pack_into` calls + ``Image.save(save_all=True)`` with this class's PIL kwargs. Pillow is + a hard picopt dependency, so this handler is always available. + + PIPELINE contains a single :class:`PILSaveTool` sentinel so the doctor + command lists this handler with a real tool entry instead of a blank + "no stages" line. The sentinel is informational only — + :meth:`ContainerHandler.optimize` calls :meth:`pack_into` directly and + never iterates the pipeline as stages. + """ + + OUTPUT_FORMAT_STR = WEBP_FORMAT_STR + OUTPUT_FILE_FORMAT = FileFormat(WEBP_FORMAT_STR, lossless=True, animated=True) + INPUT_FILE_FORMATS = frozenset( + {OUTPUT_FILE_FORMAT, *PngAnimated.INPUT_FILE_FORMATS} + ) + SUFFIXES: tuple[str, ...] = (".webp",) + PIPELINE: tuple[tuple[Tool, ...], ...] = ( + (PILSaveTool(target_format_str=WEBP_FORMAT_STR, name="pil2webp"),), + ) + + PIL2_KWARGS: MappingProxyType[str, Any] = _ANIMATED_WEBP_PIL_KWARGS + PIL2_FRAME_KWARGS: MappingProxyType[str, Any] = _ANIMATED_WEBP_FRAME_KWARGS diff --git a/picopt/plugins/webp/const.py b/picopt/plugins/webp/const.py new file mode 100644 index 0000000..6991830 --- /dev/null +++ b/picopt/plugins/webp/const.py @@ -0,0 +1,17 @@ +"""Constants shared across the WebP plugin modules.""" + +from PIL.WebPImagePlugin import WebPImageFile + +from picopt.plugins.base.format import FileFormat + +WEBP_FORMAT_STR: str = str(WebPImageFile.format) + +# cwebp >= 1.2.3 accepts PPM and TIFF input directly. Older releases need a +# pre-conversion. CWEBP_TOOL.probe() detects the version once and stores the +# result on the singleton; handlers that benefit widen their accepted input +# formats with these entries in __init__. +_PPM_FILE_FORMAT = FileFormat("PPM", lossless=True, animated=False) +_TIFF_FILE_FORMAT = FileFormat("TIFF", lossless=True, animated=False) +MODERN_CWEBP_FORMATS: frozenset[FileFormat] = frozenset( + {_PPM_FILE_FORMAT, _TIFF_FILE_FORMAT} +) diff --git a/picopt/plugins/webp/static.py b/picopt/plugins/webp/static.py new file mode 100644 index 0000000..935771d --- /dev/null +++ b/picopt/plugins/webp/static.py @@ -0,0 +1,80 @@ +"""Still (non-animated) WebP handler.""" + +from __future__ import annotations + +from types import MappingProxyType +from typing import Any + +from picopt.plugins.base import ImageHandler, PILSaveTool, Tool +from picopt.plugins.base.format import FileFormat +from picopt.plugins.png import Png +from picopt.plugins.webp.const import MODERN_CWEBP_FORMATS, WEBP_FORMAT_STR +from picopt.plugins.webp.tools import CWEBP_TOOL + + +class WebPLossless(ImageHandler): + """Lossless still WebP handler.""" + + OUTPUT_FORMAT_STR = WEBP_FORMAT_STR + OUTPUT_FILE_FORMAT = FileFormat(WEBP_FORMAT_STR, lossless=True, animated=False) + INPUT_FILE_FORMATS = frozenset({OUTPUT_FILE_FORMAT, Png.OUTPUT_FILE_FORMAT}) + SUFFIXES: tuple[str, ...] = (".webp",) + + PIL2_KWARGS: MappingProxyType[str, Any] = MappingProxyType( + {"quality": 100, "method": 6, "lossless": True} + ) + + # Tier 1: convert non-acceptable inputs to PNG via Pillow. If the input + # is already in INPUT_FILE_FORMATS this is a no-op (pil_save + # short-circuits when self.input_file_format is acceptable). + # Tier 2: encode to WebP. cwebp wins when available; otherwise Pillow + # saves directly to WebP. + PIPELINE: tuple[tuple[Tool, ...], ...] = ( + ( + PILSaveTool( + target_format_str="PNG", + save_kwargs={"compress_level": 0}, + name="pil2png", + ), + ), + ( + CWEBP_TOOL, + PILSaveTool(target_format_str=WEBP_FORMAT_STR, name="pil2webp"), + ), + ) + + # https://developers.google.com/speed/webp/docs/cwebp + _CWEBP_BASE_ARGS: tuple[str, ...] = ( + "-mt", + "-q", + "100", + "-m", + "6", + "-o", + "-", + "-quiet", + # https://groups.google.com/a/webmproject.org/g/webp-discuss/c/0GmxDmlexek + "-lossless", + "-sharp_yuv", + "-alpha_filter", + "best", + ) + _NEAR_LOSSLESS_ARGS: tuple[str, ...] = ("-near_lossless", "0") + + def __init__( + self, + *args: Any, + **kwargs: Any, + ) -> None: + """Widen acceptable inputs if cwebp is modern enough for PPM/TIFF.""" + super().__init__(*args, **kwargs) + if CWEBP_TOOL.is_modern: + self._input_file_formats = self._input_file_formats | MODERN_CWEBP_FORMATS + + def cwebp_args(self) -> tuple[str, ...]: + """Build the runtime cwebp argument tuple (no exec / input path).""" + meta = ("all",) if self.config.keep_metadata else ("none",) + args: tuple[str, ...] = (*self._CWEBP_BASE_ARGS, "-metadata", *meta) + if self.config.near_lossless: + args = (*args, *self._NEAR_LOSSLESS_ARGS) + return args diff --git a/picopt/plugins/webp/tools.py b/picopt/plugins/webp/tools.py new file mode 100644 index 0000000..60519d8 --- /dev/null +++ b/picopt/plugins/webp/tools.py @@ -0,0 +1,198 @@ +""" +WebP external tools. + +The cwebp version probe (the old ``config/cwebp.py``) is folded into +:class:`CWebPTool`. On :meth:`CWebPTool.probe`, the tool parses cwebp's +version and records whether it is modern on the probed instance. +:class:`~picopt.plugins.webp.static.WebPLossless` and +:class:`~picopt.plugins.webp.animated.Img2WebPAnimatedLossless` read +:data:`CWEBP_TOOL`'s flag in ``__init__`` to widen their accepted input +formats (skipping the intermediate PNG round-trip for PPM/TIFF). +""" + +from __future__ import annotations + +import os +import subprocess +from abc import ABC +from io import BytesIO +from pathlib import Path +from tempfile import mkstemp +from typing import BinaryIO + +from typing_extensions import override + +from picopt.plugins.base import ExternalTool, Handler, ToolStatus + + +def run_disk_input_tool( + handler: Handler, + buf: BinaryIO, + args: tuple[str, ...], +) -> BinaryIO: + """ + Drive an external tool that needs a real input file path. + + cwebp and gif2webp don't accept stdin. If ``buf`` is already a + BufferedReader on a real file we use that file's path; otherwise we + spill the buffer to a tmp file under the working directory. + """ + is_tmp = isinstance(buf, BytesIO) + if is_tmp: + wp = handler.get_working_path() + fd, tmp_str = mkstemp(prefix=wp.name + ".", suffix=handler.output_suffix) + os.close(fd) + input_path: Path | None = Path(tmp_str) + else: + input_path = handler.path_info.path + if input_path is None: + msg = "No input path available for external WebP tool" + raise ValueError(msg) + full_args = (*args, str(input_path)) + return handler.run_ext_fs(full_args, buf, input_path, input_path_tmp=is_tmp) + + +def hash_tmp_dir_suffix(handler: Handler) -> str: + """Unique-ish tmpdir suffix derived from the handler's working path.""" + wp = handler.get_working_path() + return f"{hash(str(wp.parent))}-{wp.name}" + + +class CWebPTool(ExternalTool): + """ + The ``cwebp`` external still encoder. + + Probing records on the instance whether PPM/TIFF can be passed in + directly. The doctor command will see this tool listed under + WebPLossless's PIPELINE. + """ + + name = "cwebp" + binary = "cwebp" + version_args = ("-version",) + # cwebp before this version only accepts PNG and WebP as input formats. + # Newer cwebps accept PPM and TIFF as well, which lets us skip the + # intermediate PNG round-trip when picopt is converting from those. + _MIN_CWEBP_VERSION: tuple[int, int, int] = (1, 2, 3) + + def __init__(self) -> None: + """Default to legacy so a missing cwebp never widens input formats.""" + super().__init__() + self.is_modern: bool = False + + @staticmethod + def _parse_cwebp_version(version_str: str) -> tuple[int, ...]: + """Parse '1.3.2' or '1.3.2 libwebp 1.3.2' into a tuple of ints.""" + if not version_str: + return () + head = version_str.split(maxsplit=1)[0] + parts: list[int] = [] + for part in head.split("."): + try: + parts.append(int(part)) + except ValueError: + break + return tuple(parts) + + @override + def _probe(self) -> ToolStatus: + status = super()._probe() + if status.available: + parsed = CWebPTool._parse_cwebp_version(status.version) + self.is_modern = bool(parsed) and parsed >= CWebPTool._MIN_CWEBP_VERSION + return status + + @override + def run_stage(self, handler: Handler, buf: BinaryIO) -> BinaryIO: + # Duck-typed: any handler exposing cwebp_args() qualifies. Keeps + # the tool layer from importing handler classes (import cycle). + cwebp_args = getattr(handler, "cwebp_args", None) + if cwebp_args is None: + msg = f"CWebPTool cannot run on {type(handler).__name__}" + raise TypeError(msg) + args = (*self.exec_args(), *cwebp_args()) + return run_disk_input_tool(handler, buf, args) + + +# The shared, probed-once instance. WebPLossless's PIPELINE holds it, and +# both WebPLossless and Img2WebPAnimatedLossless read its ``is_modern`` +# flag at handler construction time. +CWEBP_TOOL = CWebPTool() + + +class WebPExternalTool(ExternalTool, ABC): + """WebP Tool Suite Tool.""" + + version_args = ("-version",) + + @override + def parse_version(self, version: str) -> str: + """Version is last term.""" + version = super().parse_version(version) + return version.split()[-1] + + +class Gif2WebPTool(WebPExternalTool): + """The ``gif2webp`` external animated-GIF encoder.""" + + name = "gif2webp" + binary = "gif2webp" + + @override + def run_stage(self, handler: Handler, buf: BinaryIO) -> BinaryIO: + gif2webp_args = getattr(handler, "gif2webp_args", None) + if gif2webp_args is None: + msg = f"Gif2WebPTool cannot run on {type(handler).__name__}" + raise TypeError(msg) + args = (*self.exec_args(), *gif2webp_args()) + return run_disk_input_tool(handler, buf, args) + + +class Img2WebPTool(WebPExternalTool): + """ + The ``img2webp`` external animated-WebP packer. + + Implements ``run_pack`` rather than ``run_stage`` because img2webp's + role is to assemble already-extracted frame files into the final + animated WebP. The handler that owns this tool extracts frames to + disk during ``walk()``; ``run_pack()`` reads the frame paths off the + handler and shells out. + """ + + name = "img2webp" + binary = "img2webp" + + @override + def run_pack(self, handler: Handler) -> BinaryIO: + img2webp_args = getattr(handler, "img2webp_args", None) + if img2webp_args is None: + msg = f"Img2WebPTool cannot pack {type(handler).__name__}" + raise TypeError(msg) + args = (*self.exec_args(), *img2webp_args()) + proc = subprocess.run(args, check=True, capture_output=True) # noqa: S603 + return BytesIO(proc.stdout) + + +class WebPMuxTool(ExternalTool): + """ + The ``webpmux`` external animated-WebP demuxer + packer. + + Used for both unpacking (``webpmux -get frame N``) and packing + (``webpmux -frame ... -loop 0``). The unpack side is invoked from the + handler's overridden ``walk()``; the pack side is the standard + ``run_pack`` entry point that the handler's ``pack_into`` calls. + """ + + name = "webpmux" + binary = "webpmux" + version_args = ("-version",) + + @override + def run_pack(self, handler: Handler) -> BinaryIO: + webpmux_pack_args = getattr(handler, "webpmux_pack_args", None) + if webpmux_pack_args is None: + msg = f"WebPMuxTool cannot pack {type(handler).__name__}" + raise TypeError(msg) + args = (*self.exec_args(), *webpmux_pack_args()) + proc = subprocess.run(args, check=True, capture_output=True) # noqa: S603 + return BytesIO(proc.stdout) diff --git a/picopt/plugins/zip.py b/picopt/plugins/zip.py index a1437f4..54b2cc3 100644 --- a/picopt/plugins/zip.py +++ b/picopt/plugins/zip.py @@ -31,9 +31,43 @@ if TYPE_CHECKING: from io import BytesIO from pathlib import Path + from zipfile import ZipInfo from picopt.path import PathInfo +# Already-compressed formats gain nothing from deflate; store them. +_INCOMPRESSIBLE_SUFFIXES = frozenset( + {".avif", ".gif", ".jpeg", ".jpg", ".jxl", ".png", ".webp"} +) +_UTF8_FLAG_BIT = 0x800 + + +def _fix_zip_filename_encoding(zipinfo: ZipInfo) -> None: + """ + Repair legacy zip member names that are really UTF-8. + + Names without the UTF-8 flag are decoded as CP437 by zipfile; when the + original bytes were actually UTF-8 (common from other archivers), the + decoded string is mojibake and would be re-encoded that way forever on + repack. If the CP437 bytes round-trip strictly through UTF-8, prefer + that reading. + """ + if zipinfo.flag_bits & _UTF8_FLAG_BIT: + return + try: + raw = zipinfo.filename.encode("cp437") + fixed = raw.decode("utf-8") + except (UnicodeEncodeError, UnicodeDecodeError): + return + if fixed != zipinfo.filename: + zipinfo.filename = fixed + + +def _is_incompressible(filename: str) -> bool: + suffix = filename[filename.rfind(".") :].lower() if "." in filename else "" + return suffix in _INCOMPRESSIBLE_SUFFIXES + + # --------------------------------------------------------------------------- # Tool (always-available stdlib zipfile) # --------------------------------------------------------------------------- @@ -117,11 +151,17 @@ def _archive_for_write(self, output_buffer: BytesIO) -> ZipFile: @override def _pack_info_one_file(self, archive, path_info) -> None: - zipinfo = path_info.archiveinfo.to_zipinfo() - if not self.config.keep_metadata and ( - not zipinfo.compress_type or zipinfo.compress_type == ZIP_STORED + archiveinfo = path_info.archiveinfo + zipinfo = archiveinfo.to_zipinfo() + _fix_zip_filename_encoding(zipinfo) + if not archiveinfo.is_native_zipinfo or ( + not self.config.keep_metadata and zipinfo.compress_type == ZIP_STORED ): - zipinfo.compress_type = ZIP_DEFLATED + # Converted from another archive format (or stripping metadata): + # choose compression by content instead of the ZipInfo default. + zipinfo.compress_type = ( + ZIP_STORED if _is_incompressible(zipinfo.filename) else ZIP_DEFLATED + ) archive.writestr(zipinfo, path_info.data()) diff --git a/picopt/walk/detect_format.py b/picopt/walk/detect_format.py index 7ff9841..3c21002 100644 --- a/picopt/walk/detect_format.py +++ b/picopt/walk/detect_format.py @@ -97,7 +97,8 @@ def _is_lossless( ) -> bool: """Decide whether a PIL-identified format is the lossless variant.""" if image_format_str == _WEBP_FORMAT_STR: - return _webp_is_lossless(path_info.fp_or_buffer()) + with path_info.fp_or_buffer() as buffer: + return _webp_is_lossless(buffer) if image_format_str == _TIFF_FORMAT_STR: return is_tiff_lossless(dict(info)) return image_format_str in registry.lossless_format_strs() @@ -134,3 +135,18 @@ def detect_format( if not file_format: file_format = _get_non_pil_format(path_info) return file_format, info + + +def predetect_format(path_info: PathInfo, *, keep_metadata: bool) -> None: + """ + Detect and cache the format onto the PathInfo. + + Run inside unpack workers so the expensive PIL sniff of every archive + member parallelizes instead of serializing on the scheduler thread. + """ + try: + path_info.detected = detect_format(path_info, keep_metadata=keep_metadata) + except Exception: + # Leave undetected: the main thread re-detects inside its + # per-member error guard and reports the failure there. + path_info.detected = None diff --git a/picopt/walk/dir_timestamps.py b/picopt/walk/dir_timestamps.py new file mode 100644 index 0000000..ac9ce73 --- /dev/null +++ b/picopt/walk/dir_timestamps.py @@ -0,0 +1,103 @@ +"""Per-directory completion tracking for compacted timestamp writes.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + + from treestamps import Grovestamps + + +@dataclass +class _DirTracker: + """Track pending children of a walked directory for timestamp writes.""" + + top_path: Path + pending: int = 0 + sealed: bool = False + # A child errored: writing a compacted dir timestamp would cover the + # failed file and permanently skip it on subsequent runs. + errored: bool = False + + +class DirTimestamper: + """ + Write compacted directory timestamps once a directory fully completes. + + The walk layer announces directories (``begin_dir`` … ``seal_dir``) and + the scheduler reports each top-level child's completion; when a sealed + directory has no pending children and none errored, its timestamp is + written with compaction and completion cascades to the parent tracker. + """ + + def __init__(self, timestamps: Grovestamps | None) -> None: + """Store the timestamps sink.""" + self._timestamps = timestamps + self._trackers: dict[Path, _DirTracker] = {} + + def begin_dir(self, top_path: Path, dir_path: Path) -> None: + """Register a directory whose children are about to be enqueued.""" + parent_tracker = self._trackers.get(dir_path.parent) + if parent_tracker is not None: + parent_tracker.pending += 1 + self._trackers[dir_path] = _DirTracker(top_path=top_path) + + def seal_dir(self, dir_path: Path) -> None: + """Mark a directory as fully enumerated; finalize if nothing pends.""" + tracker = self._trackers.get(dir_path) + if tracker is None: + return + tracker.sealed = True + if tracker.pending <= 0: + self._finalize(dir_path, tracker) + + def cancel_dir(self, dir_path: Path) -> None: + """Remove a directory tracker without finalizing (walk errors).""" + tracker = self._trackers.pop(dir_path, None) + if tracker is None: + return + parent_dir = dir_path.parent + if parent_dir in self._trackers: + self.child_done(parent_dir) + + def enqueue_child(self, child_path: Path) -> None: + """Increment a directory's pending count for a newly enqueued child.""" + tracker = self._trackers.get(child_path.parent) + if tracker is not None: + tracker.pending += 1 + + def mark_errored(self, dir_path: Path) -> None: + """Poison a directory's timestamp write after a child error.""" + tracker = self._trackers.get(dir_path) + if tracker is not None: + tracker.errored = True + + def child_done(self, parent_dir: Path, *, errored: bool = False) -> None: + """Decrement a directory's pending count; finalize when ready.""" + tracker = self._trackers.get(parent_dir) + if tracker is None: + return + if errored: + tracker.errored = True + tracker.pending -= 1 + if tracker.pending <= 0 and tracker.sealed: + self._finalize(parent_dir, tracker) + + def __contains__(self, dir_path: Path) -> bool: + """Whether a directory is currently tracked.""" + return dir_path in self._trackers + + def _finalize(self, dir_path: Path, tracker: _DirTracker) -> None: + """Write directory timestamp with compaction and cascade to parent.""" + del self._trackers[dir_path] + # An errored child means this directory is not fully optimized; a + # compacted dir stamp would cover the failed file and skip it on + # every future run. Per-file stamps for the successes remain. + if self._timestamps and not tracker.errored: + self._timestamps.set(tracker.top_path, dir_path, compact=True) + parent_dir = dir_path.parent + if parent_dir in self._trackers: + self.child_done(parent_dir, errored=tracker.errored) diff --git a/picopt/walk/handler_factory.py b/picopt/walk/handler_factory.py index 4fe6f99..1bccf72 100644 --- a/picopt/walk/handler_factory.py +++ b/picopt/walk/handler_factory.py @@ -31,6 +31,7 @@ from loguru import logger from picopt import plugins as registry +from picopt.archive_stamps import ArchiveStamps from picopt.plugins.base import ( ArchiveHandler, ContainerHandler, @@ -50,6 +51,26 @@ from picopt.plugins.base.format import FileFormat +def _build_archive_stamps( + path_info: PathInfo, timestamps: Grovestamps | None +) -> ArchiveStamps | None: + """ + Build the picklable timestamps slice an archive's unpack worker uses. + + Seeds the slice with the tree's serialized stamps (``dump_dict`` — + the same config-headed mapping treestamps round-trips through disk) + so member entry-mtime skip checks work in the worker, where the real + Grovestamps cannot travel. + """ + if timestamps is None: + return None + try: + tree = timestamps[path_info.top_path] + except KeyError: + return None + return ArchiveStamps(tree._config, tree.dump_dict()) # noqa: SLF001 + + class HandlerFactory: """Handler factory for creating format-appropriate handlers.""" @@ -60,11 +81,12 @@ def __init__(self, config: PicoptSettings, reporter: Reporter) -> None: def _lookup_route( self, + config: PicoptSettings, file_format: FileFormat | None, ) -> tuple | None: if not file_format: return None - if file_format.format_str not in self._config.formats: + if file_format.format_str not in config.formats: return None routes = registry.routes_by_format() @@ -73,88 +95,32 @@ def _lookup_route( return None return entry - def _is_pipeline_available(self, handler_cls: type[Handler]) -> bool: - """ - Whether the config-time probe found a workable pipeline for this handler. - - A handler is "available" iff every tier in its ``PIPELINE`` produced a - selected tool. Handlers with an empty PIPELINE (e.g. archive handlers - that pack via Python libraries, or PILPack sentinels) are always - available — there is nothing to be missing. - """ - if not handler_cls.PIPELINE: - return True - return handler_cls in self._config.computed.handler_stages - - def _pick_handler_class_choose_converter( - self, candidate: type[Handler], convert_to: frozenset[str] - ) -> type[Handler] | None: - if candidate.OUTPUT_FORMAT_STR not in convert_to: - return None - if not self._is_pipeline_available(candidate): - return None - return candidate - - def _pick_handler_class_converter( - self, - file_format: FileFormat | None, - convert_chain: tuple[type[Handler], ...], - *, - convert: bool, - repack: bool, - ) -> type[Handler] | None: - # Conversion path: archives only convert during the repack pass; for - # images we prefer the convert handler at first sight because it's - # often faster than translating through PIL. - if not file_format: - return None - handler_cls: type[Handler] | None = None - convert_to: frozenset[str] = frozenset(self._config.convert_to or ()) - if convert and (not file_format.archive or repack): - for candidate in convert_chain: - if handler_cls := self._pick_handler_class_choose_converter( - candidate, convert_to - ): - break - return handler_cls - def _pick_handler_class( self, + config: PicoptSettings, file_format: FileFormat | None, *, convert: bool, repack: bool = False, ) -> type[Handler] | None: """Return a handler class for the file format.""" - entry = self._lookup_route(file_format) + entry = self._lookup_route(config, file_format) if not entry: return None native, convert_chain = entry - - handler_cls = self._pick_handler_class_converter( - file_format, convert_chain, convert=convert, repack=repack + return registry.pick_route_handler( + file_format, + native, + convert_chain, + convert=convert, + repack=repack, + convert_to=frozenset(config.convert_to or ()), + handler_stages=config.computed.handler_stages, ) - # Native fallback. - if ( - handler_cls is None - and native is not None - and self._is_pipeline_available(native) - ): - handler_cls = native - - # Repack callers need a packing-capable handler. - if ( - repack - and handler_cls is not None - and not (issubclass(handler_cls, ContainerHandler) and handler_cls.CAN_PACK) - ): - handler_cls = None - - return handler_cls - def _get_repack_handler_class( self, + config: PicoptSettings, path_info: PathInfo, file_format: FileFormat, ) -> type[ContainerHandler] | None: @@ -162,6 +128,7 @@ def _get_repack_handler_class( repack_handler_class: type[ContainerHandler] | None = None try: picked = self._pick_handler_class( + config, file_format, convert=path_info.convert, repack=True, @@ -176,11 +143,7 @@ def _get_repack_handler_class( logger.warning(msg) print_exc() - if ( - not repack_handler_class - and self._config.verbose > 1 - and not self._config.list_only - ): + if not repack_handler_class and config.verbose > 1 and not config.list_only: fmt = str(file_format) if file_format else "unknown" msg = ( f"Skip: ({fmt}) is not an enabled image or container format: " @@ -191,14 +154,18 @@ def _get_repack_handler_class( return repack_handler_class def _create_handler_get_class_and_format( - self, path_info: PathInfo + self, config: PicoptSettings, path_info: PathInfo ) -> tuple[FileFormat | None, type[Handler] | None, Mapping[str, Any]]: handler_cls: type[Handler] | None = None try: - file_format, info = detect_format( - path_info, keep_metadata=self._config.keep_metadata - ) + # Unpack workers pre-detect archive members so the expensive + # PIL sniff doesn't serialize on this thread. + detected = path_info.detected + if detected is None: + detected = detect_format(path_info, keep_metadata=config.keep_metadata) + file_format, info = detected handler_cls = self._pick_handler_class( + config, file_format, convert=path_info.convert, ) @@ -213,13 +180,21 @@ def create_handler( self, path_info: PathInfo, timestamps: Grovestamps | None = None, + settings: PicoptSettings | None = None, ) -> Handler | None: - """Return a handler for the image format.""" + """ + Return a handler for the image format. + + ``settings`` carries a directory's resolved ``.picopt.yaml`` + configuration; the handler is constructed with it so per-directory + options travel into workers and archive members. + """ if path_info.noop: return None + config = settings or self._config file_format, handler_cls, info = self._create_handler_get_class_and_format( - path_info + config, path_info ) if not handler_cls or not file_format: @@ -230,17 +205,17 @@ def create_handler( kwargs["info"] = info if issubclass(handler_cls, ContainerHandler): repack_handler_class = self._get_repack_handler_class( - path_info, file_format + config, path_info, file_format ) if repack_handler_class: kwargs["repack_handler_class"] = repack_handler_class if issubclass(handler_cls, ArchiveHandler): - kwargs["timestamps"] = timestamps + kwargs["timestamps"] = _build_archive_stamps(path_info, timestamps) else: return None return handler_cls( - self._config, + config, path_info, input_file_format=file_format, **kwargs, diff --git a/picopt/walk/legacy_timestamps.py b/picopt/walk/legacy_timestamps.py index 65fac2e..f2228ed 100644 --- a/picopt/walk/legacy_timestamps.py +++ b/picopt/walk/legacy_timestamps.py @@ -4,6 +4,8 @@ from typing import TYPE_CHECKING +from loguru import logger + from picopt.path import is_path_case_sensitive, is_path_ignored if TYPE_CHECKING: @@ -42,19 +44,30 @@ def _import_old_parent_timestamps(self) -> None: def _import_old_child_timestamps(self) -> None: stack = [self._root_dir] + visited: set[tuple[int, int]] = set() while stack: path = stack.pop() - if not self._config.symlinks and path.is_symlink(): - continue - if path.is_dir(): - if not is_path_ignored( - self._config, path, ignore_case=self._ignore_case - ): - stack.extend(path.iterdir()) - elif path.name == OLD_TIMESTAMPS_NAME: - self._add_old_timestamp(path) - # consume child timestamps - self._timestamps._consumed_paths.add(path) # noqa: SLF001 + try: + if not self._config.symlinks and path.is_symlink(): + continue + if path.is_dir(): + # Symlink cycles must not walk forever. + dir_stat = path.stat() + dir_key = (dir_stat.st_dev, dir_stat.st_ino) + if dir_key in visited: + continue + visited.add(dir_key) + if not is_path_ignored( + self._config, path, ignore_case=self._ignore_case + ): + stack.extend(path.iterdir()) + elif path.name == OLD_TIMESTAMPS_NAME: + self._add_old_timestamp(path) + # consume child timestamps + self._timestamps._consumed_paths.add(path) # noqa: SLF001 + except OSError as exc: + # An unreadable directory is not worth aborting the run. + logger.warning(f"Legacy timestamp import skipping {path}: {exc}") def import_old_timestamps(self) -> None: """Import all old timestamps.""" @@ -66,4 +79,6 @@ def __init__(self, config: PicoptSettings, timestamps: Treestamps) -> None: self._config: PicoptSettings = config self._timestamps: Treestamps = timestamps self._root_dir: Path = self._timestamps.root_dir - self._ignore_case: bool = is_path_case_sensitive(self._root_dir) + # Ignore patterns match case-insensitively only on filesystems + # that are themselves case-insensitive. + self._ignore_case: bool = not is_path_case_sensitive(self._root_dir) diff --git a/picopt/walk/scheduler.py b/picopt/walk/scheduler.py index eb8a31b..b000ee4 100644 --- a/picopt/walk/scheduler.py +++ b/picopt/walk/scheduler.py @@ -35,6 +35,8 @@ from typing import TYPE_CHECKING from picopt.report import ReportStats +from picopt.walk.detect_format import predetect_format +from picopt.walk.dir_timestamps import DirTimestamper if TYPE_CHECKING: from collections.abc import Callable @@ -50,6 +52,17 @@ # --------------------------------------------------------------------- state +# Multiplier from a top-level item's on-disk size to its estimated peak resident +# memory while optimized. A container holds, concurrently: the whole archive +# bytes, every decompressed member, the optimized copies, and the output buffer +# built at repack — plus a pickled duplicate of much of that in the worker +# process. Measured peak RSS / on-disk size is ~3x for comic archives, so the +# sum of charges approximates real memory use and ``--memory-limit`` reads as an +# approximate RAM target. It only needs to be roughly right: the memory gate +# always lets a single over-budget item run alone, so an underestimate never +# deadlocks, it just overshoots once. +_MEM_COST_FACTOR = 3 + class NodeState(Enum): """Lifecycle of a ContainerNode.""" @@ -82,12 +95,11 @@ class UnpackResult: pre-walk() state. Attributes the worker's walk() may have mutated on the handler that - matter to the main thread (grep `ContainerHandler` subclasses for the - exact field names; they vary per concrete class): + matter to the main thread: - - staging dir path (attribute name varies: `_tmp_path`, `_unpack_dir`, - etc. — whatever the concrete handler uses internally). The main - thread needs this to rmtree on cleanup or rollback. + - staging dir path — read uniformly via ``get_staging_dir()`` and + stored on the node. The main thread needs this to rmtree on + cleanup or rollback. - `comment` (bytes | None) — archive comment extracted during walk. `create_repack_handler` reads this and passes it to the repack handler constructor. @@ -119,6 +131,11 @@ def run(self) -> UnpackResult: """Unpack the container and list its children. Worker-side.""" try: children = list(self.handler.walk()) + # Pre-detect member formats here so the expensive PIL sniff + # parallelizes instead of serializing on the scheduler thread. + keep_metadata = self.handler.config.keep_metadata + for child in children: + predetect_format(child, keep_metadata=keep_metadata) return UnpackResult(handler=self.handler, children=children) except Exception as exc: traceback.print_exc() @@ -170,7 +187,9 @@ class ContainerNode: children: list[ContainerNode] = field(default_factory=list) state: NodeState = NodeState.NEW had_work: bool = False # any child produced replacement bytes + had_error: bool = False # any child errored; don't timestamp this subtree staging_dir: Path | None = None + cost: int = 0 # memory budget charged for this node (0 = not charged) def is_top_level(self) -> bool: """Return True if this node has no container parent.""" @@ -186,15 +205,7 @@ class _LeafEntry: job: OptimizeLeafJob parent: ContainerNode | None # None = direct directory leaf, not in container - - -@dataclass -class _DirTracker: - """Track pending children of a walked directory for timestamp writes.""" - - top_path: Path - pending: int = 0 - sealed: bool = False + cost: int = 0 # memory budget charged for a standalone leaf (0 otherwise) # ------------------------------------------------------------------- scheduler @@ -235,14 +246,26 @@ def __init__( self._child_enqueue_callback = child_enqueue_callback self._ready: deque[tuple[Job, ContainerNode | None]] = deque() + # Top-level items deferred by the memory gate wait here in FIFO + # order instead of being re-scanned through _ready on every tick. + self._gated: deque[tuple[Job, ContainerNode | None]] = deque() self._inflight_unpack: dict[Future, ContainerNode] = {} self._inflight_leaf: dict[Future, _LeafEntry] = {} self._inflight_repack: dict[Future, ContainerNode] = {} self._live_nodes: set[ContainerNode] = set() - self._dir_trackers: dict[Path, _DirTracker] = {} + self._dirs = DirTimestamper(timestamps) self._fail_fast_triggered: bool = False + # Memory-aware admission. `_byte_budget <= 0` disables the gate. + # `_inflight_bytes` is the sum of estimated resident memory for every + # live top-level item (containers still being unpacked/optimized/ + # repacked, plus standalone leaf images). New top-level items are only + # admitted while they fit the budget — except when nothing is live, so a + # single archive larger than the whole budget still runs (alone). + self._byte_budget: int = config.memory_limit + self._inflight_bytes: int = 0 + # ---------------------------------------------------------- public API def enqueue_leaf( self, job: OptimizeLeafJob, parent: ContainerNode | None = None @@ -252,7 +275,7 @@ def enqueue_leaf( if parent is not None: parent.pending += 1 elif job.path_info.path is not None: - self._dir_enqueue(job.path_info.path) + self._dirs.enqueue_child(job.path_info.path) def enqueue_container( self, handler: ContainerHandler, parent: ContainerNode | None = None @@ -264,7 +287,7 @@ def enqueue_container( parent.children.append(node) parent.pending += 1 elif handler.path_info.path is not None: - self._dir_enqueue(handler.path_info.path) + self._dirs.enqueue_child(handler.path_info.path) self._ready.append((UnpackJob(handler=handler), node)) return node @@ -278,36 +301,25 @@ def accept_prebuilt_report(self, report: ReportStats, top_path: Path) -> None: """ self._record_totals(report) self._write_timestamp(report, top_path) + if report.exc and report.path is not None: + self._dirs.mark_errored(report.path.parent) def begin_dir(self, top_path: Path, dir_path: Path) -> None: """Register a directory whose children are about to be enqueued.""" - parent_tracker = self._dir_trackers.get(dir_path.parent) - if parent_tracker is not None: - parent_tracker.pending += 1 - self._dir_trackers[dir_path] = _DirTracker(top_path=top_path) + self._dirs.begin_dir(top_path, dir_path) def seal_dir(self, dir_path: Path) -> None: """Mark a directory as fully enumerated; finalize if no pending children.""" - tracker = self._dir_trackers.get(dir_path) - if tracker is None: - return - tracker.sealed = True - if tracker.pending <= 0: - self._finalize_dir(dir_path, tracker) + self._dirs.seal_dir(dir_path) def cancel_dir(self, dir_path: Path) -> None: """Remove a directory tracker without finalizing (used on walk errors).""" - tracker = self._dir_trackers.pop(dir_path, None) - if tracker is None: - return - parent_dir = dir_path.parent - if parent_dir in self._dir_trackers: - self._dir_child_done(parent_dir) + self._dirs.cancel_dir(dir_path) def run(self) -> None: - """Drain ready and inflight until both are empty.""" + """Drain ready, gated, and inflight until all are empty.""" try: - while self._ready or self._inflight_count() > 0: + while self._ready or self._gated or self._inflight_count() > 0: self._submit_ready() if self._inflight_count() == 0: continue @@ -339,8 +351,7 @@ def _inflight_count(self) -> int: + len(self._inflight_repack) ) - @staticmethod - def _drop_cancelled_ready_job(job: Job, node: ContainerNode) -> None: + def _drop_cancelled_ready_job(self, job: Job, node: ContainerNode) -> None: """ Decrement parent pending counter for a leaf of a cancelled subtree. @@ -352,7 +363,7 @@ def _drop_cancelled_ready_job(job: Job, node: ContainerNode) -> None: case UnpackJob() | RepackJob(): pass case _: - node.pending = max(0, node.pending - 1) + self._child_done(node) def _track_submitted_job( self, fut: Future, job: Job, node: ContainerNode | None @@ -372,21 +383,98 @@ def _track_submitted_job( node.state = NodeState.REPACKING self._inflight_repack[fut] = node - def _submit_ready_job(self) -> None: - """Pop and submit one job from the ready dequeue.""" - job, node = self._ready.popleft() - # Skip jobs whose owning node got cancelled while they were queued. - if node is not None and node.state is NodeState.CANCELLED: - self._drop_cancelled_ready_job(job, node) - return + def _est_cost(self, path_info: PathInfo) -> int: + """Estimate peak resident memory for a top-level item, in bytes.""" + return int(path_info.bytes_in()) * _MEM_COST_FACTOR + + def _charge_info(self, job: Job, node: ContainerNode | None) -> tuple[bool, int]: + """ + Whether a ready job introduces a *new* top-level item, and its cost. + + Only new top-level containers (their UnpackJob) and standalone + directory-level leaves are charged/gated. Jobs that make progress on an + already-admitted container (in-container leaves, nested-container + unpacks, and every RepackJob) are exempt — their memory is already + accounted for by their top-level ancestor, and gating them could + deadlock the container that must repack to release budget. + """ + match job: + case UnpackJob(): + if node is not None and node.is_top_level(): + return True, self._est_cost(node.handler.path_info) + case OptimizeLeafJob(): + if node is None: # standalone directory leaf + return True, self._est_cost(job.path_info) + case _: # RepackJob and progress on already-admitted containers + pass + return False, 0 + + def _admits(self, cost: int) -> bool: + """Whether a new top-level item costing `cost` may start now.""" + if self._byte_budget <= 0: + return True # gate disabled + if self._inflight_bytes == 0: + return True # forward-progress guarantee: run it alone + return self._inflight_bytes + cost <= self._byte_budget + + def _release_budget(self, cost: int) -> None: + self._inflight_bytes = max(0, self._inflight_bytes - cost) + + def _retire_node(self, node: ContainerNode) -> None: + """Release a node's memory charge and drop it from the live set.""" + self._release_budget(node.cost) + node.cost = 0 + self._live_nodes.discard(node) + + def _submit_one(self, job: Job, node: ContainerNode | None, cost: int) -> None: + """Submit one admitted job and charge its budget (if any).""" fut = self._executor.submit(job.run) self._track_submitted_job(fut, job, node) + if cost: + self._inflight_bytes += cost + if isinstance(job, UnpackJob): + assert node is not None + node.cost = cost + else: # standalone leaf + self._inflight_leaf[fut].cost = cost + + def _submit_gated(self, cap: int) -> None: + """Admit memory-gated items in FIFO order until the head blocks.""" + while self._gated and self._inflight_count() < cap: + job, node = self._gated[0] + if node is not None and node.state is NodeState.CANCELLED: + self._gated.popleft() + self._drop_cancelled_ready_job(job, node) + continue + _, cost = self._charge_info(job, node) + if not self._admits(cost): + break + self._gated.popleft() + self._submit_one(job, node, cost) def _submit_ready(self) -> None: - """Submit ready jobs up to the backpressure cap.""" + """ + Submit ready jobs up to the backpressure cap and the memory budget. + + Top-level items blocked by the memory budget move to the gated + queue so exempt jobs behind them (leaves/repacks of already-admitted + containers) still run — those are what eventually complete and free + budget. The gated queue is retried head-first each tick instead of + rescanning the whole ready queue. + """ cap = 2 * self._max_workers + self._submit_gated(cap) while self._ready and self._inflight_count() < cap: - self._submit_ready_job() + job, node = self._ready.popleft() + # Skip jobs whose owning node got cancelled while they were queued. + if node is not None and node.state is NodeState.CANCELLED: + self._drop_cancelled_ready_job(job, node) + continue + charged, cost = self._charge_info(job, node) + if charged and not self._admits(cost): + self._gated.append((job, node)) + continue + self._submit_one(job, node, cost if charged else 0) def _cancel_subtree( self, root: ContainerNode, *, reason: BaseException | None @@ -403,12 +491,13 @@ def _cancel_subtree( node.state = NodeState.CANCELLED node.handler.get_optimized_contents().clear() stack.extend(node.children) - # Purge ready queue of anything belonging to a cancelled node. + # Purge queues of anything belonging to a cancelled node. self._ready = deque((job, n) for (job, n) in self._ready if n not in cancelled) + self._gated = deque((job, n) for (job, n) in self._gated if n not in cancelled) # Clean staging immediately for every cancelled node. for node in cancelled: self._cleanup_node_staging(node) - self._live_nodes.discard(node) + self._retire_node(node) # Already-running futures check state on completion and drop results. def _trigger_fail_fast(self, reason: BaseException | None) -> None: @@ -418,6 +507,7 @@ def _trigger_fail_fast(self, reason: BaseException | None) -> None: for top in tops: self._cancel_subtree(top, reason=reason) self._ready.clear() + self._gated.clear() def _handle_completion(self, fut: Future) -> None: """Dispatch one completed future by which inflight map owns it.""" @@ -456,6 +546,7 @@ def _handle_unpack_done(self, node: ContainerNode, result: UnpackResult) -> None # walk()-mutated twin. See UnpackResult docstring for which # attributes this restores. node.handler = result.handler + node.staging_dir = result.handler.get_staging_dir() if node.state is NodeState.CANCELLED: self._cleanup_node_staging(node) @@ -481,6 +572,8 @@ def _handle_unpack_done(self, node: ContainerNode, result: UnpackResult) -> None def _handle_leaf_done(self, entry: _LeafEntry, report: ReportStats) -> None: """Process an OptimizeLeafJob completion.""" + # Release a standalone leaf's memory charge (0 for in-container leaves). + self._release_budget(entry.cost) parent = entry.parent # In-container leaf: hydrate PathInfo from bytes, stash in parent. @@ -488,7 +581,7 @@ def _handle_leaf_done(self, entry: _LeafEntry, report: ReportStats) -> None: if parent.state is NodeState.CANCELLED: # drop on the floor, but still decrement so parent can # eventually finalize (its own cancel walk will handle it) - parent.pending = max(0, parent.pending - 1) + self._child_done(parent) return if report.exc is None: parent.handler.hydrate_optimized_path_info(entry.job.path_info, report) @@ -496,17 +589,22 @@ def _handle_leaf_done(self, entry: _LeafEntry, report: ReportStats) -> None: if report.changed: parent.had_work = True else: - # leaf error inside a container — record, keep going + # leaf error inside a container — record it, but keep the + # member's walk-time bytes so repack doesn't drop it from + # the rebuilt archive. self._reporter.record_report(report) - parent.pending = max(0, parent.pending - 1) - self._maybe_start_repack(parent) + parent.handler.get_optimized_contents().add(entry.job.path_info) + parent.had_error = True + self._child_done(parent) return # Top-level directory leaf: straight to totals + timestamps. self._record_totals(report) self._write_timestamp(report, entry.job.path_info.top_path) if entry.job.path_info.path is not None: - self._dir_child_done(entry.job.path_info.path.parent) + self._dirs.child_done( + entry.job.path_info.path.parent, errored=bool(report.exc) + ) def _handle_repack_failure(self, report: ReportStats, node: ContainerNode) -> None: if self._config.fail_fast: @@ -520,20 +618,32 @@ def _handle_repack_failure(self, report: ReportStats, node: ContainerNode) -> No root = root.parent self._cancel_subtree(root, reason=report.exc) self._reporter.record_report(report) + self._notify_dir_of_top_level_error(root) return # default rollback: this container becomes one error, parent # sees it as a "done" child with no work. self._cancel_subtree(node, reason=report.exc) self._reporter.record_report(report) if node.parent is not None: - node.parent.pending = max(0, node.parent.pending - 1) - self._maybe_start_repack(node.parent) + # Keep the nested container's original bytes so the parent's + # repack doesn't drop this member from the rebuilt archive. + node.parent.handler.get_optimized_contents().add(node.handler.path_info) + node.parent.had_error = True + self._child_done(node.parent) + else: + self._notify_dir_of_top_level_error(node) + + def _notify_dir_of_top_level_error(self, node: ContainerNode) -> None: + """Tell a failed top-level container's directory tracker it is done.""" + path = node.handler.path_info.path + if path is not None: + self._dirs.child_done(path.parent, errored=True) def _handle_repack_done(self, node: ContainerNode, report: ReportStats) -> None: """Process a RepackJob completion (or synthesized no-op/error).""" if node.state is NodeState.CANCELLED: self._cleanup_node_staging(node) - self._live_nodes.discard(node) + self._retire_node(node) return # Failure branches @@ -545,32 +655,50 @@ def _handle_repack_done(self, node: ContainerNode, report: ReportStats) -> None: self._record_totals(report) if node.is_top_level(): top_path = node.handler.path_info.top_path - self._write_timestamp(report, top_path) + # A member error inside means the container isn't fully + # optimized; timestamping it would skip the failed member + # forever on subsequent runs. + if not node.had_error: + self._write_timestamp(report, top_path) self._cleanup_node_staging(node) if node.handler.path_info.path is not None: - self._dir_child_done(node.handler.path_info.path.parent) + self._dirs.child_done( + node.handler.path_info.path.parent, errored=node.had_error + ) else: # Hydrate a PathInfo for our parent's _optimized_contents so - # the parent's repack picks up our repacked bytes. + # the parent's repack picks up our repacked bytes and any + # conversion rename (performed on the worker's pickled copy). parent = node.parent assert parent is not None - if report.data: - node.handler.path_info.set_data(report.data) + parent.handler.hydrate_optimized_path_info(node.handler.path_info, report) parent.handler.get_optimized_contents().add(node.handler.path_info) if report.changed: parent.had_work = True + if node.had_error: + parent.had_error = True # Our staging lives until the PARENT's repack reads us, so we # don't rmtree here. Parent's repack completion triggers it. - parent.pending = max(0, parent.pending - 1) - self._maybe_start_repack(parent) + self._child_done(parent) node.state = NodeState.DONE - self._live_nodes.discard(node) + self._retire_node(node) # Clean up child staging dirs now that we've finished repacking. for child in node.children: # node.children is all ContainerNodes no check needed self._cleanup_node_staging(child) + def _child_done(self, node: ContainerNode) -> None: + """ + One child of ``node`` finished (or was dropped); maybe repack. + + The single place a container's pending counter decrements. + _maybe_start_repack() itself refuses cancelled/repacking/done + nodes, so this is safe on every completion path. + """ + node.pending = max(0, node.pending - 1) + self._maybe_start_repack(node) + def _maybe_start_repack(self, node: ContainerNode) -> None: """If pending == 0, enqueue RepackJob or synthesize no-op completion.""" if node.pending != 0 or node.state is NodeState.CANCELLED: @@ -582,9 +710,15 @@ def _maybe_start_repack(self, node: ContainerNode) -> None: # whether any child produced replacement bytes. Handlers like # Img2WebPAnimated set _do_repack=True unconditionally during walk() # because format conversion always requires repacking even when no - # individual child was "optimized". + # individual child was "optimized". A requested container format + # conversion (e.g. CBR -> CBZ) also needs a repack even when every + # member was already optimized. if not node.handler.is_do_repack(): - node.handler.set_do_repack(do_repack=node.had_work) + convert_intent = ( + node.handler.repack_handler_class is not None + and type(node.handler) is not node.handler.repack_handler_class + ) + node.handler.set_do_repack(do_repack=node.had_work or convert_intent) if not node.handler.is_do_repack(): # No work: synthesize a no-op completion so the parent chain # gets notified identically to a real repack. @@ -597,7 +731,8 @@ def _maybe_start_repack(self, node: ContainerNode) -> None: self._handle_repack_done(node, noop) return - repack_handler = self._create_repack_handler(self._config, node.handler) + # Repack under the handler's own (per-directory) config. + repack_handler = self._create_repack_handler(node.handler.config, node.handler) node.handler = repack_handler self._ready.append((RepackJob(handler=repack_handler), node)) @@ -610,30 +745,6 @@ def _write_timestamp(self, report: ReportStats, top_path: Path) -> None: if self._timestamps and report.path is not None and not report.exc: self._timestamps.set(top_path, report.path) - def _dir_enqueue(self, child_path: Path) -> None: - """Increment a directory's pending count for a newly enqueued child.""" - tracker = self._dir_trackers.get(child_path.parent) - if tracker is not None: - tracker.pending += 1 - - def _dir_child_done(self, parent_dir: Path) -> None: - """Decrement a directory's pending count; finalize when ready.""" - tracker = self._dir_trackers.get(parent_dir) - if tracker is None: - return - tracker.pending -= 1 - if tracker.pending <= 0 and tracker.sealed: - self._finalize_dir(parent_dir, tracker) - - def _finalize_dir(self, dir_path: Path, tracker: _DirTracker) -> None: - """Write directory timestamp with compaction and cascade to parent.""" - del self._dir_trackers[dir_path] - if self._timestamps: - self._timestamps.set(tracker.top_path, dir_path, compact=True) - parent_dir = dir_path.parent - if parent_dir in self._dir_trackers: - self._dir_child_done(parent_dir) - def _cleanup_node_staging(self, node: ContainerNode) -> None: """Rmtree this node's staging_dir, swallowing errors.""" if node.staging_dir is None: diff --git a/picopt/walk/skip.py b/picopt/walk/skip.py index d7aa365..914f957 100644 --- a/picopt/walk/skip.py +++ b/picopt/walk/skip.py @@ -3,20 +3,22 @@ from __future__ import annotations import shutil +from pathlib import Path +from stat import S_ISDIR, S_ISREG from typing import TYPE_CHECKING from loguru import logger from treestamps import Treestamps from picopt import PROGRAM_NAME, WORKING_SUFFIX +from picopt.config.consts import DIR_CONFIG_FILENAME from picopt.path import PathInfo, is_path_ignored from picopt.walk.legacy_timestamps import OLD_TIMESTAMPS_NAME if TYPE_CHECKING: - from pathlib import Path - from treestamps import Grovestamps + from picopt.archive_stamps import ArchiveStamps from picopt.config.settings import PicoptSettings from picopt.log.reporter import Reporter @@ -27,18 +29,21 @@ class WalkSkipper: _TIMESTAMPS_FILENAMES: frozenset[str] = frozenset( {*Treestamps.get_filenames(PROGRAM_NAME), OLD_TIMESTAMPS_NAME} ) + # picopt's own metadata files, never optimization targets. Public so + # Walk._count can mirror the gate. + SKIP_FILENAMES: frozenset[str] = _TIMESTAMPS_FILENAMES | {DIR_CONFIG_FILENAME} def __init__( self, config: PicoptSettings, reporter: Reporter, - timestamps: Grovestamps | None = None, + timestamps: Grovestamps | ArchiveStamps | None = None, *, in_archive: bool = False, ) -> None: """Initialize.""" self._config: PicoptSettings = config - self._timestamps: Grovestamps | None = timestamps + self._timestamps: Grovestamps | ArchiveStamps | None = timestamps self._in_archive: bool = in_archive self._reporter: Reporter = reporter @@ -66,21 +71,32 @@ def _is_skippable(self, path_info: PathInfo) -> bool: path = path_info.path # File types + basename = Path(path_info.name()).name if not self._config.symlinks and path and path.is_symlink(): reason = "symlink" - elif path_info.name() in self._TIMESTAMPS_FILENAMES: - legacy = "legacy " if path_info.name() == OLD_TIMESTAMPS_NAME else "" + elif basename == DIR_CONFIG_FILENAME: + reason = "picopt config" + elif basename in self._TIMESTAMPS_FILENAMES: + legacy = "legacy " if basename == OLD_TIMESTAMPS_NAME else "" reason = f"{legacy}timestamp" elif is_path_ignored( self._config, path_info.archive_pseudo_path(), - ignore_case=path_info.is_case_sensitive, + ignore_case=not path_info.is_case_sensitive, ): reason = "ignored" elif not self._in_archive and path and not path.exists(): # Check disk last for performance reason = "not found" warn = True + elif ( + not self._in_archive + and path + and (fs_stat := path_info.stat()) is not None + and not (S_ISREG(fs_stat.st_mode) or S_ISDIR(fs_stat.st_mode)) + ): + # FIFOs, sockets, device nodes: opening a FIFO blocks forever. + reason = "not a regular file" self._log_skip(reason, path_info, warn=warn) diff --git a/picopt/walk/walk.py b/picopt/walk/walk.py index 7955ea0..adab0ec 100644 --- a/picopt/walk/walk.py +++ b/picopt/walk/walk.py @@ -5,14 +5,17 @@ import os import traceback from concurrent.futures import ProcessPoolExecutor +from hashlib import sha256 from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from loguru import logger from treestamps import Grovestamps, GrovestampsConfig, Treestamps from picopt import PROGRAM_NAME -from picopt.config.consts import TIMESTAMPS_CONFIG_KEYS +from picopt.config import PicoptConfig +from picopt.config.consts import DIR_CONFIG_FILENAME, TIMESTAMPS_CONFIG_KEYS +from picopt.config.dirconfig import DirConfig from picopt.exceptions import PicoptError from picopt.log import console from picopt.log.progress import make_progress @@ -28,6 +31,8 @@ from picopt.walk.skip import WalkSkipper if TYPE_CHECKING: + from argparse import Namespace + from picopt.config.settings import PicoptSettings @@ -52,7 +57,9 @@ def _create_top_paths( raise PicoptError(msg) return tuple(top_paths) - def __init__(self, config: PicoptSettings) -> None: + def __init__( + self, config: PicoptSettings, arguments: Namespace | None = None + ) -> None: """Initialize.""" self._config: PicoptSettings = config self._top_paths: tuple[Path, ...] = self._create_top_paths() @@ -70,6 +77,73 @@ def __init__(self, config: PicoptSettings) -> None: self._timestamps: Grovestamps | None = None # reassigned at start of run self._skipper: WalkSkipper = WalkSkipper(config, self._reporter) self._handler_factory: HandlerFactory = HandlerFactory(config, self._reporter) + # Per-directory .picopt.yaml resolution; args re-layer so CLI wins. + self._dirconfig: DirConfig = DirConfig( + PicoptConfig(), arguments, config, self._stats + ) + self._dir_skippers: dict[Path, WalkSkipper] = {} + # (st_dev, st_ino) of every walked directory; symlink cycles and + # duplicate links must not re-optimize the same tree. + self._visited_dirs: set[tuple[int, int]] = set() + + def _dir_skipper(self, top_path: Path, dir_path: Path) -> WalkSkipper: + """Return the skipper for a directory's resolved settings. Cached.""" + settings = self._dirconfig.get_settings(top_path, dir_path) + if settings is self._config: + return self._skipper + if (skipper := self._dir_skippers.get(dir_path)) is None: + skipper = WalkSkipper(settings, self._reporter, self._timestamps) + self._dir_skippers[dir_path] = skipper + return skipper + + @staticmethod + def _config_candidates(root: Path, *, is_dir: bool) -> list[Path]: + """List candidate ``.picopt.yaml`` files for one target path.""" + # A single-file target only sees its own directory's config. + if not is_dir: + return [root.parent / DIR_CONFIG_FILENAME] + try: + return sorted(root.rglob(DIR_CONFIG_FILENAME)) + except OSError: + return [] + + @staticmethod + def _config_chunk(root: Path, config_file: Path, *, is_dir: bool) -> bytes | None: + """Return one config file's fingerprint contribution, or None.""" + try: + data = config_file.read_bytes() + except OSError: + # Covers missing files and directories named like the config. + return None + # A path relative to the target keeps the digest stable across + # cwd/mount changes; add/remove/rename still flips it. + rel = config_file.relative_to(root) if is_dir else config_file.name + return str(rel).encode() + b"\0" + data + b"\0" + + def _dir_config_fingerprint(self) -> str: + """ + Hash every ``.picopt.yaml`` under the target paths. + + Folded into the treestamps ``program_config`` so that editing, + adding, or removing any directory config flips the digest and the + affected tree re-processes on the next run — over-invalidation + that is always safe (re-checking an already-optimized file is a + cheap skip) and never wrong-skips a file whose effective config + changed. + """ + hasher = sha256() + seen: set[Path] = set() + for path in self._config.paths: + root = Path(path) + is_dir = root.is_dir() + for config_file in self._config_candidates(root, is_dir=is_dir): + if config_file in seen: + continue + seen.add(config_file) + chunk = self._config_chunk(root, config_file, is_dir=is_dir) + if chunk is not None: + hasher.update(chunk) + return hasher.hexdigest() def _init_timestamps(self) -> None: """Init timestamps.""" @@ -79,9 +153,14 @@ def _init_timestamps(self) -> None: # shallow Mapping with only those keys so we don't have to serialize # the whole frozen dataclass (especially the computed sub-fields, # which carry re.Pattern objects and class-keyed dicts). - program_config = { + program_config: dict[str, Any] = { key: getattr(self._config, key) for key in TIMESTAMPS_CONFIG_KEYS } + # Fold a fingerprint of the directory configs into the program + # config so any change to a .picopt.yaml invalidates its tree's + # timestamps (the single global program_config can't otherwise see + # per-directory config changes). + program_config["_dir_config_fingerprint"] = self._dir_config_fingerprint() config = GrovestampsConfig( paths=self._top_paths, program_name=PROGRAM_NAME, @@ -90,7 +169,7 @@ def _init_timestamps(self) -> None: ignore=self._config.ignore, check_config=self._config.timestamps_check_config, program_config=program_config, - program_config_keys=TIMESTAMPS_CONFIG_KEYS, + program_config_keys=TIMESTAMPS_CONFIG_KEYS | {"_dir_config_fingerprint"}, ) self._timestamps = Grovestamps(config) for timestamps in self._timestamps.values(): @@ -113,7 +192,25 @@ def _enqueue_children( ) -> None: """Bridge between scheduler and HandlerFactory for container children.""" for path_info in children: - handler = self._create_handler(path_info) + try: + # Members inherit their container's (per-directory) config; + # nested containers pass it down automatically. + handler = self._create_handler(path_info, node.handler.config) + except Exception as exc: + # A member that can't even be sniffed (e.g. a decompression + # bomb) is one error, not a run-ender. Pass it through + # unmodified so the repacked archive keeps it. + traceback.print_exc() + report = ReportStats( + path=Path(path_info.full_output_name()), + bytes_in=path_info.bytes_in(), + exc=exc, + config=self._config, + path_info=path_info, + ) + self._reporter.record_report(report) + node.handler.get_optimized_contents().add(path_info) + continue if handler is None: # noop copy — child passes through unmodified node.handler.get_optimized_contents().add(path_info) @@ -128,13 +225,28 @@ def _enqueue_children( def walk_dir(self, dir_path_info: PathInfo, scheduler: Scheduler) -> None: """Recursively walk a directory, enqueuing jobs into the scheduler.""" - if not self._config.recurse or not dir_path_info.is_dir(): + if not dir_path_info.is_dir(): return dir_path = dir_path_info.path if not dir_path: return + # The directory's own .picopt.yaml governs recursion into it. + settings = self._dirconfig.get_settings(dir_path_info.top_path, dir_path) + if not settings.recurse: + return + + try: + dir_stat = dir_path.stat() + except OSError: + return + dir_key = (dir_stat.st_dev, dir_stat.st_ino) + if dir_key in self._visited_dirs: + logger.debug(f"Skip: directory already visited (symlink loop): {dir_path}") + return + self._visited_dirs.add(dir_key) + scheduler.begin_dir(dir_path_info.top_path, dir_path) try: files = [] @@ -175,8 +287,12 @@ def _handle_file( msg = f"Bad picopt handler {handler}" raise TypeError(msg) - def _create_handler(self, path_info: PathInfo) -> Handler | None: - handler = self._handler_factory.create_handler(path_info, self._timestamps) + def _create_handler( + self, path_info: PathInfo, settings: PicoptSettings | None = None + ) -> Handler | None: + handler = self._handler_factory.create_handler( + path_info, self._timestamps, settings=settings + ) if handler is None: return None @@ -188,18 +304,25 @@ def _create_handler(self, path_info: PathInfo) -> Handler | None: def _walk_file_get_handler( self, path_info: PathInfo, scheduler: Scheduler ) -> Handler | None: + settings: PicoptSettings | None = None if path_info.frame is None: - if self._skipper.is_walk_file_skip(path_info): + skipper = self._skipper + if path_info.path is not None: + dir_path = Treestamps.get_dir(path_info.path) + skipper = self._dir_skipper(path_info.top_path, dir_path) + settings = self._dirconfig.get_settings(path_info.top_path, dir_path) + + if skipper.is_walk_file_skip(path_info): return None if path_info.is_dir(): self.walk_dir(path_info, scheduler) return None - if self._skipper.is_older_than_timestamp(path_info): + if skipper.is_older_than_timestamp(path_info): return None - handler = self._create_handler(path_info) + handler = self._create_handler(path_info, settings) if not handler: logger.debug(f"Skip: no handler: {path_info.full_output_name()}") self._stats.record_skipped() @@ -229,22 +352,41 @@ def _walk_top_path(self, top_path: Path, scheduler: Scheduler) -> None: ) self.walk_file(path_info, scheduler) - def _count(self, path: Path, name: str, *, is_symlink: bool, is_dir: bool) -> int: + def _count( + self, + top_path: Path, + path: Path, + name: str, + visited: set[tuple[int, int]], + *, + is_symlink: bool, + is_dir: bool, + ) -> int: """ Count progress-bar advances for ``path``. Pre-resolved ``is_symlink`` / ``is_dir`` come from ``os.scandir`` on recursive calls so deep trees don't pay an extra ``stat`` per entry. """ + # Mirror the walk's per-directory settings (cached in DirConfig). + settings = self._dirconfig.get_settings( + top_path, path if is_dir else path.parent + ) if ( - not self._config.recurse - or (not self._config.symlinks and is_symlink) - or name in WalkSkipper._TIMESTAMPS_FILENAMES # noqa: SLF001 + not settings.recurse + or (not settings.symlinks and is_symlink) + or name in WalkSkipper.SKIP_FILENAMES or not is_dir - or is_path_ignored(self._config, path, ignore_case=False) + or is_path_ignored(settings, path, ignore_case=False) ): return 1 try: + # Mirror walk_dir's symlink-loop guard or a cycle never ends. + dir_stat = path.stat() + dir_key = (dir_stat.st_dev, dir_stat.st_ino) + if dir_key in visited: + return 0 + visited.add(dir_key) with os.scandir(path) as it: entries = sorted(it, key=lambda e: e.name) except OSError: @@ -253,8 +395,10 @@ def _count(self, path: Path, name: str, *, is_symlink: bool, is_dir: bool) -> in for entry in entries: try: total += self._count( + top_path, Path(entry.path), entry.name, + visited, is_symlink=entry.is_symlink(), is_dir=entry.is_dir(), ) @@ -274,8 +418,10 @@ def _count_path(self, path: Path) -> int: """ try: return self._count( + Treestamps.get_dir(path), path, path.name, + set(), is_symlink=path.is_symlink(), is_dir=path.is_dir(), ) @@ -289,10 +435,14 @@ def _count_total(self) -> int: def walk(self) -> Stats: """Optimize all configured files.""" self._init_timestamps() + self._visited_dirs.clear() max_workers = self._config.jobs or os.cpu_count() or 1 - total = self._count_total() - progress = make_progress(console, enabled=self._config.verbose > 0, total=total) + # The pre-count re-walks the whole tree; don't pay for it when the + # progress bar isn't shown at all. + progress_enabled = self._config.verbose > 0 + total = self._count_total() if progress_enabled else 0 + progress = make_progress(console, enabled=progress_enabled, total=total) # Replace the no-op progress that the skipper / factory captured at # construction time so they advance the real bar. self._reporter.progress = progress diff --git a/pyproject.toml b/pyproject.toml index 83f0913..acc7113 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "rich-argparse~=1.8", "rich~=15.0", "ruamel-yaml>=0.18,<1.0", - "treestamps~=4.0.1", + "treestamps~=4.1.0", "typing-extensions~=4.13", ] description = "A multi format lossless image optimizer that uses external tools" @@ -36,7 +36,8 @@ readme = "README.md" requires-python = ">=3.10" license = { text = "GPL-3.0-only" } name = "picopt" -version = "6.5.2" +version = "6.6.0" + [[project.authors]] name = "AJ Slater" email = "aj@slater.net" @@ -65,7 +66,7 @@ build = [] lint = [ "basedpyright~=1.38", "codespell~=2.4", - "complexipy~=5.1", + "complexipy~=6.0", "icecream~=2.1", "mbake~=1.4.5", "pathspec~=1.1.0", @@ -75,12 +76,7 @@ lint = [ "types-python-dateutil~=2.9", "vulture~=2.3", ] -test = [ - "coverage[toml]~=7.0", - "pytest-asyncio~=1.0", - "pytest-cov~=7.0", - "pytest-gitignore~=1.3", -] +test = ["coverage[toml]~=7.0", "pytest-asyncio~=1.0", "pytest-cov~=7.0"] docs = ["mkapi~=4.4", "mkdocs-minify-plugin~=0.8", "mkdocs~=1.6"] ci = ["requests~=2.32"] @@ -324,6 +320,7 @@ ignore-variadic-names = true ignore-overlong-task-comments = true [tool.ruff.lint.per-file-ignores] +"bin/*" = ["T201", "T203"] "tests/*" = ["D101", "D102", "SLF001", "T201", "T203"] [tool.ruff.lint.pylint] diff --git a/tests/test_archive_fidelity.py b/tests/test_archive_fidelity.py new file mode 100644 index 0000000..5fd4d93 --- /dev/null +++ b/tests/test_archive_fidelity.py @@ -0,0 +1,188 @@ +"""Test archive metadata fidelity through repack: comments, order, times, compression.""" + +import shutil +from collections.abc import Iterator +from io import BytesIO +from tarfile import LNKTYPE, SYMTYPE, TarFile, TarInfo +from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile, ZipInfo + +import pytest +from py7zr import SevenZipFile +from py7zr.helpers import ArchiveTimestamp + +from picopt import PROGRAM_NAME, cli +from picopt.archiveinfo import ArchiveInfo +from picopt.plugins.zip import _fix_zip_filename_encoding +from tests import IMAGES_DIR, get_test_dir + +__all__ = () + +TMP_ROOT = get_test_dir() +PNG_FN = "test_png.png" +COMMENT = b"ComicBookInfo metadata lives here" +MEMBER_ORDER = ("zzz_first.txt", PNG_FN, "aaa_last.txt") +TEXT_DATA = b"some text that deflates well " * 10 +KNOWN_MTIME = 946684800.0 # 2000-01-01T00:00:00Z +MTIME_TOLERANCE_SECS = 2.0 + + +def _run_picopt(*args: str) -> None: + cli.main((PROGRAM_NAME, *args, str(TMP_ROOT))) + + +class TestArchiveFidelity: + """Repacked archives must preserve comments, order, and sane metadata.""" + + @pytest.fixture(autouse=True) + def _setup_and_teardown(self) -> Iterator[None]: + shutil.rmtree(TMP_ROOT, ignore_errors=True) + TMP_ROOT.mkdir(parents=True) + yield + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + def test_zip_comment_preserved_on_repack(self) -> None: + """An archive comment survives optimization.""" + zip_path = TMP_ROOT / "commented.cbz" + with ZipFile(zip_path, "w") as zf: + zf.write(IMAGES_DIR / PNG_FN, PNG_FN) + zf.comment = COMMENT + _run_picopt("-rvx", "CBZ,PNG") + with ZipFile(zip_path, "r") as zf: + assert zf.comment == COMMENT + assert zf.namelist() == [PNG_FN] + + def test_member_order_preserved_on_repack(self) -> None: + """Members come back in their original, non-alphabetical order.""" + zip_path = TMP_ROOT / "ordered.cbz" + with ZipFile(zip_path, "w") as zf: + for name in MEMBER_ORDER: + if name == PNG_FN: + zf.write(IMAGES_DIR / PNG_FN, PNG_FN) + else: + zf.writestr(name, TEXT_DATA) + _run_picopt("-rvx", "CBZ,PNG") + with ZipFile(zip_path, "r") as zf: + assert tuple(zf.namelist()) == MEMBER_ORDER + + def test_pre_1980_member_times_clamped_on_conversion(self) -> None: + """Tar members with epoch-zero mtimes convert without crashing.""" + tar_path = TMP_ROOT / "old.tar" + with TarFile(tar_path, "w") as tf: + info = TarInfo("ancient.txt") + info.size = len(TEXT_DATA) + info.mtime = 0 + tf.addfile(info, BytesIO(TEXT_DATA)) + _run_picopt("-rvx", "TAR,ZIP", "-c", "ZIP") + zip_path = TMP_ROOT / "old.zip" + assert zip_path.exists() + with ZipFile(zip_path, "r") as zf: + zipinfo = zf.getinfo("ancient.txt") + assert zipinfo.date_time == (1980, 1, 1, 0, 0, 0) + assert zf.read("ancient.txt") == TEXT_DATA + + def test_conversion_compresses_by_content(self) -> None: + """Converted members deflate text but store already-compressed images.""" + tar_path = TMP_ROOT / "mixed.tar" + with TarFile(tar_path, "w") as tf: + tf.add(IMAGES_DIR / PNG_FN, PNG_FN) + info = TarInfo("notes.txt") + info.size = len(TEXT_DATA) + tf.addfile(info, BytesIO(TEXT_DATA)) + _run_picopt("-rvx", "TAR,ZIP", "-c", "ZIP") + zip_path = TMP_ROOT / "mixed.zip" + assert zip_path.exists() + with ZipFile(zip_path, "r") as zf: + assert zf.getinfo("notes.txt").compress_type == ZIP_DEFLATED + assert zf.getinfo(PNG_FN).compress_type == ZIP_STORED + + def test_tar_links_survive_repack(self) -> None: + """Symlinks and hardlinks keep their type and target through repack.""" + tar_path = TMP_ROOT / "links.tar" + with TarFile(tar_path, "w") as tf: + tf.add(IMAGES_DIR / PNG_FN, PNG_FN) + info = TarInfo("notes.txt") + info.size = len(TEXT_DATA) + tf.addfile(info, BytesIO(TEXT_DATA)) + symlink = TarInfo("link_to_notes.txt") + symlink.type = SYMTYPE + symlink.linkname = "notes.txt" + tf.addfile(symlink) + hardlink = TarInfo("hardlink_to_notes.txt") + hardlink.type = LNKTYPE + hardlink.linkname = "notes.txt" + tf.addfile(hardlink) + orig_size = tar_path.stat().st_size + + _run_picopt("-rvx", "TAR,PNG") + + assert tar_path.stat().st_size < orig_size + with TarFile(tar_path, "r") as tf: + symlink = tf.getmember("link_to_notes.txt") + assert symlink.type == SYMTYPE + assert symlink.linkname == "notes.txt" + assert symlink.size == 0 + hardlink = tf.getmember("hardlink_to_notes.txt") + assert hardlink.type == LNKTYPE + assert hardlink.linkname == "notes.txt" + assert hardlink.size == 0 + member = tf.extractfile("notes.txt") + assert member is not None + assert member.read() == TEXT_DATA + + def test_seven_zip_member_mtimes_survive_repack(self) -> None: + """7z member modification times are restored after repack.""" + sz_path = TMP_ROOT / "timed.7z" + png_data = (IMAGES_DIR / PNG_FN).read_bytes() + stamp = ArchiveTimestamp.from_datetime(KNOWN_MTIME) + with SevenZipFile(sz_path, "w") as szf: + szf.writef(BytesIO(png_data), PNG_FN) + header = szf.header + assert header is not None + assert header.files_info is not None + file_info = header.files_info.files[-1] + file_info["creationtime"] = stamp + file_info["lastwritetime"] = stamp + file_info["lastaccesstime"] = stamp + orig_size = sz_path.stat().st_size + + _run_picopt("-rvx", "7Z,PNG") + + assert sz_path.stat().st_size < orig_size + with SevenZipFile(sz_path, "r") as szf: + info = szf.list()[0] + assert info.creationtime is not None + assert ( + abs(info.creationtime.timestamp() - KNOWN_MTIME) < MTIME_TOLERANCE_SECS + ) + + +class TestZipFilenameEncoding: + """Legacy UTF-8 names misread as CP437 are repaired at repack.""" + + def test_mojibake_name_repaired(self) -> None: + real_name = "日本語.png" + mojibake = real_name.encode("utf-8").decode("cp437") + zipinfo = ZipInfo(mojibake) + _fix_zip_filename_encoding(zipinfo) + assert zipinfo.filename == real_name + + def test_utf8_flagged_name_untouched(self) -> None: + zipinfo = ZipInfo("日本語.png") + zipinfo.flag_bits |= 0x800 + _fix_zip_filename_encoding(zipinfo) + assert zipinfo.filename == "日本語.png" + + def test_ascii_name_untouched(self) -> None: + zipinfo = ZipInfo("plain.png") + _fix_zip_filename_encoding(zipinfo) + assert zipinfo.filename == "plain.png" + + +class TestArchiveInfoClamp: + """to_zipinfo clamps un-representable member times.""" + + def test_tarinfo_epoch_zero_clamps_to_1980(self) -> None: + info = TarInfo("x.txt") + info.mtime = 0 + zipinfo = ArchiveInfo(info).to_zipinfo() + assert tuple(zipinfo.date_time) == (1980, 1, 1, 0, 0, 0) diff --git a/tests/test_archive_timestamps.py b/tests/test_archive_timestamps.py new file mode 100644 index 0000000..c3ed0ba --- /dev/null +++ b/tests/test_archive_timestamps.py @@ -0,0 +1,174 @@ +"""Test worker-side in-archive entry skipping and timestamp consumption.""" + +import pickle +import shutil +from collections.abc import Iterator +from zipfile import ZipFile, ZipInfo + +import pytest +from treestamps.tree.config import TreestampsConfig + +from picopt import PROGRAM_NAME, cli +from picopt.archive_stamps import ArchiveStamps +from picopt.config import PicoptConfig +from picopt.config.settings import PicoptSettings +from picopt.path import PathInfo +from picopt.plugins.zip import Cbz +from tests import IMAGES_DIR, get_test_dir + +__all__ = () + +TMP_ROOT = get_test_dir() +PNG_BYTES = (IMAGES_DIR / "test_png.png").read_bytes() +STAMP = 1_900_000_000.0 # 2030 — after OLD_DATE, before NEW_DATE +OLD_DATE = (2020, 1, 1, 0, 0, 0) +NEW_DATE = (2035, 1, 1, 0, 0, 0) +CBZ_FN = "stamped.cbz" + + +def _make_config(*args: str) -> PicoptSettings: + return PicoptConfig().get_config( + cli.get_arguments(("picopt", "-rtvx", "CBZ,PNG", *args, str(TMP_ROOT))) + ) + + +def _make_cbz(members: dict[str, tuple], extra: dict[str, bytes] | None = None): + zip_path = TMP_ROOT / CBZ_FN + with ZipFile(zip_path, "w") as zf: + for name, date_time in members.items(): + zf.writestr(ZipInfo(name, date_time=date_time), PNG_BYTES) + for name, data in (extra or {}).items(): + zf.writestr(ZipInfo(name, date_time=OLD_DATE), data) + return zip_path + + +def _make_handler(zip_path, config: PicoptSettings, stamps: ArchiveStamps | None): + path_info = PathInfo(top_path=TMP_ROOT, path=zip_path, convert=False) + handler = Cbz( + config, + path_info, + input_file_format=Cbz.OUTPUT_FILE_FORMAT, + timestamps=stamps, + repack_handler_class=Cbz, + ) + # walk() always executes in a worker; prove the machinery survives it. + return pickle.loads(pickle.dumps(handler)) # noqa: S301 + + +def _tree_config() -> TreestampsConfig: + return TreestampsConfig( + program_name=PROGRAM_NAME, path=TMP_ROOT, check_config=False + ) + + +def _walk_names(handler) -> tuple[set[str], set[str]]: + yielded = {pi.name() for pi in handler.walk()} + copied = {pi.name() for pi in handler.get_optimized_contents()} + return yielded, copied + + +class TestInArchiveEntrySkip: + """Members older than the archive's stamp skip optimization.""" + + @pytest.fixture(autouse=True) + def _setup_and_teardown(self) -> Iterator[None]: + shutil.rmtree(TMP_ROOT, ignore_errors=True) + TMP_ROOT.mkdir(parents=True) + yield + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + def test_old_entries_skip_and_new_entries_process(self) -> None: + zip_path = _make_cbz({"old.png": OLD_DATE, "new.png": NEW_DATE}) + stamps = ArchiveStamps(_tree_config(), {str(zip_path): STAMP}) + handler = _make_handler(zip_path, _make_config(), stamps) + yielded, copied = _walk_names(handler) + assert yielded == {"new.png"} + assert copied == {"old.png"} + + def test_ignore_entry_mtimes_flag_processes_everything(self) -> None: + zip_path = _make_cbz({"old.png": OLD_DATE, "new.png": NEW_DATE}) + stamps = ArchiveStamps(_tree_config(), {str(zip_path): STAMP}) + handler = _make_handler(zip_path, _make_config("-E"), stamps) + yielded, copied = _walk_names(handler) + assert yielded == {"old.png", "new.png"} + assert not copied + + def test_no_stamps_processes_everything(self) -> None: + zip_path = _make_cbz({"old.png": OLD_DATE, "new.png": NEW_DATE}) + handler = _make_handler(zip_path, _make_config(), None) + yielded, copied = _walk_names(handler) + assert yielded == {"old.png", "new.png"} + assert not copied + + +class TestInArchiveConsume: + """.picopt_treestamps files inside archives are consumed and dropped.""" + + @pytest.fixture(autouse=True) + def _setup_and_teardown(self) -> Iterator[None]: + shutil.rmtree(TMP_ROOT, ignore_errors=True) + TMP_ROOT.mkdir(parents=True) + yield + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + def test_consumed_stamps_skip_members_and_drop_stamp_file(self) -> None: + stamps = ArchiveStamps(_tree_config(), {}) + yaml_str = f"old.png: {STAMP}\n" + zip_path = _make_cbz( + {"old.png": OLD_DATE, "new.png": NEW_DATE}, + extra={stamps.filename: yaml_str.encode()}, + ) + handler = _make_handler(zip_path, _make_config(), stamps) + yielded, copied = _walk_names(handler) + assert yielded == {"new.png"} + assert copied == {"old.png"} # stamp member dropped, not copied + assert handler.is_do_repack() + + def test_consumed_stamps_rejected_when_config_mismatch(self) -> None: + tree_config = TreestampsConfig( + program_name=PROGRAM_NAME, + path=TMP_ROOT, + check_config=True, + program_config={"bigger": False}, + program_config_keys=("bigger",), + ) + stamps = ArchiveStamps(tree_config, {}) + yaml_str = f"config:\n bigger: true\nold.png: {STAMP}\n" + zip_path = _make_cbz( + {"old.png": OLD_DATE}, + extra={stamps.filename: yaml_str.encode()}, + ) + handler = _make_handler(zip_path, _make_config(), stamps) + yielded, _ = _walk_names(handler) + # The stamp does not match this run's config: old.png must process. + assert yielded == {"old.png"} + + +class TestIncrementalArchiveRuns: + """End to end: a member added with an old entry mtime is skipped.""" + + @pytest.fixture(autouse=True) + def _setup_and_teardown(self) -> Iterator[None]: + shutil.rmtree(TMP_ROOT, ignore_errors=True) + TMP_ROOT.mkdir(parents=True) + yield + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + def test_second_run_skips_old_dated_member(self) -> None: + zip_path = _make_cbz({"first.png": (2025, 1, 1, 0, 0, 0)}) + cli.main((PROGRAM_NAME, "-rtvx", "CBZ,PNG", str(TMP_ROOT))) + + # Modify the archive: add an unoptimized member with an OLD entry + # date. The archive's mtime is now newer than its stamp. + with ZipFile(zip_path, "a") as zf: + zf.writestr(ZipInfo("late.png", date_time=OLD_DATE), PNG_BYTES) + + cli.main((PROGRAM_NAME, "-rtvx", "CBZ,PNG", str(TMP_ROOT))) + with ZipFile(zip_path, "r") as zf: + # Entry mtime predates the stamp: member passed through as-is. + assert zf.read("late.png") == PNG_BYTES + + cli.main((PROGRAM_NAME, "-rtvEx", "CBZ,PNG", str(TMP_ROOT))) + with ZipFile(zip_path, "r") as zf: + # -E ignores entry mtimes: the member finally gets optimized. + assert len(zf.read("late.png")) < len(PNG_BYTES) diff --git a/tests/test_atomic_replace.py b/tests/test_atomic_replace.py new file mode 100644 index 0000000..c066c7a --- /dev/null +++ b/tests/test_atomic_replace.py @@ -0,0 +1,197 @@ +"""Test the replace/discard policy of Handler._cleanup_after_optimize.""" + +import os +from io import BytesIO +from pathlib import Path +from subprocess import CalledProcessError +from typing import BinaryIO + +import pytest +from typing_extensions import override + +from picopt import WORKING_SUFFIX +from picopt.config.settings import ComputedSettings, IgnorePatterns, PicoptSettings +from picopt.path import PathInfo +from picopt.plugins.base.format import FileFormat +from picopt.plugins.base.handler import Handler + +__all__ = () + +_ORIGINAL_DATA = b"original image data" * 64 +_OPTIMIZED_DATA = b"optimized" + +_PNG_FORMAT = FileFormat("PNG", lossless=True, animated=False) + + +class _StubHandler(Handler): + """Concrete Handler; optimize() is bypassed in these tests.""" + + OUTPUT_FORMAT_STR = "PNG" + OUTPUT_FILE_FORMAT = _PNG_FORMAT + SUFFIXES = (".png",) + + @override + def optimize(self) -> BinaryIO: + """Unused; tests call _cleanup_after_optimize directly.""" + raise NotImplementedError + + +class _StubConvertHandler(_StubHandler): + """Converting handler: .png input becomes a .webp final path.""" + + OUTPUT_FORMAT_STR = "WEBP" + OUTPUT_FILE_FORMAT = FileFormat("WEBP", lossless=True, animated=False) + SUFFIXES = (".webp",) + + +def _make_settings(*, dry_run: bool = False, preserve: bool = False) -> PicoptSettings: + computed = ComputedSettings( + handler_stages={}, ignore=IgnorePatterns(case=None, ignore_case=None) + ) + return PicoptSettings( + bigger=False, + dry_run=dry_run, + fail_fast=False, + fail_fast_container=False, + ignore_defaults=True, + jobs=1, + keep_metadata=True, + list_only=False, + memory_limit=0, + near_lossless=False, + png_max=False, + preserve=preserve, + recurse=True, + symlinks=True, + timestamps=False, + timestamps_check_config=True, + timestamps_ignore_archive_entry_mtimes=False, + verbose=0, + disable_programs=(), + formats=(), + ignore=(), + paths=(), + after=None, + convert_to=None, + extra_formats=None, + computed=computed, + ) + + +def _make_handler( + tmp_path: Path, + handler_cls: type[_StubHandler] = _StubHandler, + *, + dry_run: bool = False, + preserve: bool = False, +) -> tuple[_StubHandler, Path]: + original_path = tmp_path / "test.png" + original_path.write_bytes(_ORIGINAL_DATA) + path_info = PathInfo(top_path=tmp_path, path=original_path, convert=False) + handler = handler_cls( + _make_settings(dry_run=dry_run, preserve=preserve), path_info, _PNG_FORMAT + ) + return handler, original_path + + +def test_replace_success_is_atomic_and_clean(tmp_path: Path) -> None: + """A smaller result replaces the original and leaves no temp files.""" + handler, original_path = _make_handler(tmp_path) + report = handler._cleanup_after_optimize(BytesIO(_OPTIMIZED_DATA)) + assert report.exc is None + assert original_path.read_bytes() == _OPTIMIZED_DATA + assert not list(tmp_path.glob(f"*{WORKING_SUFFIX}*")) + + +def test_crash_during_replace_preserves_original( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A crash at the final rename must leave the original file intact.""" + handler, original_path = _make_handler(tmp_path) + + def _crash(self: Path, _target: Path) -> Path: + msg = f"simulated crash replacing {self}" + raise OSError(msg) + + monkeypatch.setattr(Path, "replace", _crash) + with pytest.raises(OSError, match="simulated crash"): + handler._cleanup_after_optimize(BytesIO(_OPTIMIZED_DATA)) + assert original_path.read_bytes() == _ORIGINAL_DATA + + +def test_write_failure_preserves_original( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A failure while writing new data (e.g. ENOSPC) must not touch the original.""" + handler, original_path = _make_handler(tmp_path) + real_open = Path.open + + def _no_space(self: Path, mode: str = "r", *args, **kwargs) -> object: + if "w" in mode: + msg = f"simulated disk full writing {self}" + raise OSError(msg) + return real_open(self, mode, *args, **kwargs) + + monkeypatch.setattr(Path, "open", _no_space) + with pytest.raises(OSError, match="simulated disk full"): + handler._cleanup_after_optimize(BytesIO(_OPTIMIZED_DATA)) + assert original_path.read_bytes() == _ORIGINAL_DATA + + +def test_discarded_bigger_conversion_is_a_skip_not_an_error(tmp_path: Path) -> None: + """A conversion result bigger than the input is discarded quietly.""" + handler, original_path = _make_handler(tmp_path, _StubConvertHandler) + bigger_data = _ORIGINAL_DATA + b"bloat" + report = handler._cleanup_after_optimize(BytesIO(bigger_data)) + assert report.exc is None + assert not report.converted + assert not report.changed + assert report.path == original_path + assert original_path.read_bytes() == _ORIGINAL_DATA + assert not (tmp_path / "test.webp").exists() + # path_info must still point at the file that actually exists + assert handler.path_info.path == original_path + + +def test_dry_run_conversion_writes_nothing(tmp_path: Path) -> None: + """Dry-run reports the would-be result without touching the filesystem.""" + handler, original_path = _make_handler(tmp_path, _StubConvertHandler, dry_run=True) + report = handler._cleanup_after_optimize(BytesIO(_OPTIMIZED_DATA)) + assert report.exc is None + assert not report.converted + assert not report.changed + assert original_path.read_bytes() == _ORIGINAL_DATA + assert not (tmp_path / "test.webp").exists() + assert not list(tmp_path.glob(f"*{WORKING_SUFFIX}*")) + + +def test_preserve_survives_chown_failure( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """--preserve still restores mode and mtime when chown is denied.""" + handler, original_path = _make_handler(tmp_path, preserve=True) + original_mtime_ns = original_path.stat().st_mtime_ns + + def _denied(*_args, **_kwargs) -> None: + msg = "Operation not permitted" + raise PermissionError(msg) + + monkeypatch.setattr(os, "chown", _denied) + report = handler._cleanup_after_optimize(BytesIO(_OPTIMIZED_DATA)) + assert report.exc is None + assert original_path.read_bytes() == _OPTIMIZED_DATA + assert original_path.stat().st_mtime_ns == original_mtime_ns + + +def test_run_ext_fs_cleans_temp_input_when_tool_fails(tmp_path: Path) -> None: + """A failed external tool must not leave the spilled temp input behind.""" + handler, _ = _make_handler(tmp_path) + input_path = tmp_path / "spilled.tmp" + with pytest.raises(CalledProcessError): + handler.run_ext_fs( + ("/usr/bin/false",), + BytesIO(_ORIGINAL_DATA), + input_path, + input_path_tmp=True, + ) + assert not input_path.exists() diff --git a/tests/test_cli.py b/tests/test_cli.py index bcabcef..f8c7018 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -10,6 +10,8 @@ from pathlib import Path from picopt import cli +from picopt.config import PicoptConfig +from picopt.config.consts import TIMESTAMPS_CONFIG_KEYS from picopt.plugins.zip import Cbz, Zip from tests import IMAGES_DIR, get_test_dir @@ -55,7 +57,9 @@ def test_get_arguments(self: Any) -> None: Cbz.OUTPUT_FORMAT_STR, Zip.OUTPUT_FORMAT_STR, ) - assert arguments.symlinks + # Un-passed flags stay None so config-file/env layers aren't + # shadowed; the config layer's defaults resolve symlinks to True. + assert arguments.symlinks is None self._test_get_arguments_aux(arguments) def test_main(self: Any) -> None: @@ -74,3 +78,20 @@ def test_main_err(self: Any) -> None: cli.main() except SystemExit as exc: assert exc.code == 1 # noqa: PT017 + + def test_memory_limit_parsing(self: Any) -> None: + """--memory-limit accepts suffixed sizes and resolves to bytes.""" + for arg, expected in (("8G", 8 * 1024**3), ("512M", 512 * 1024**2)): + args = cli.get_arguments(("picopt", "--memory-limit", arg, str(TMP_ROOT))) + config = PicoptConfig().get_config(args) + assert config.memory_limit == expected + + def test_memory_limit_auto(self: Any) -> None: + """The default (0/auto) resolves to a positive byte budget.""" + args = cli.get_arguments(("picopt", str(TMP_ROOT))) + config = PicoptConfig().get_config(args) + assert config.memory_limit > 0 + + def test_memory_limit_not_in_timestamp_config(self: Any) -> None: + """memory_limit must not invalidate existing timestamps.""" + assert "memory_limit" not in TIMESTAMPS_CONFIG_KEYS diff --git a/tests/test_config_cli.py b/tests/test_config_cli.py new file mode 100644 index 0000000..cd94951 --- /dev/null +++ b/tests/test_config_cli.py @@ -0,0 +1,152 @@ +"""Test config layering, validation errors, and doctor tier accounting.""" + +from pathlib import Path +from typing import Any + +import pytest +from confuse.exceptions import ConfigError +from typing_extensions import override + +from picopt import cli +from picopt.config import PicoptConfig +from picopt.doctor import PicoptDoctor +from picopt.plugins.base.tool import Tool, ToolStatus + +__all__ = () + +CONFIG_FILE_VERBOSE = 2 + + +def _get_settings(*args: str): + return PicoptConfig().get_config(cli.get_arguments(("picopt", *args, "."))) + + +class TestConfigLayering: + """Config-file values must not be shadowed by un-passed CLI flags.""" + + def test_config_file_booleans_respected(self, tmp_path: Path) -> None: + cfg = tmp_path / "picopt.yaml" + cfg.write_text("picopt:\n recurse: true\n bigger: true\n verbose: 2\n") + settings = _get_settings("-C", str(cfg)) + assert settings.recurse is True + assert settings.bigger is True + assert settings.verbose == CONFIG_FILE_VERBOSE + + def test_cli_flags_still_override_config_file(self, tmp_path: Path) -> None: + cfg = tmp_path / "picopt.yaml" + cfg.write_text("picopt:\n recurse: false\n timestamps: false\n") + settings = _get_settings("-C", str(cfg), "-r", "-t") + assert settings.recurse is True + assert settings.timestamps is True + + def test_defaults_without_config_file(self) -> None: + settings = _get_settings() + assert settings.recurse is False + assert settings.bigger is False + assert settings.verbose == 1 + assert settings.keep_metadata is True + assert settings.symlinks is True + + +class TestConfigValidation: + """Bad values abort at startup with a clean error, not deep in the run.""" + + def test_unparseable_memory_limit_aborts(self) -> None: + with pytest.raises(ConfigError, match="memory-limit"): + _get_settings("--memory-limit", "banana") + + def test_unparseable_after_aborts(self) -> None: + with pytest.raises(ConfigError, match="after"): + _get_settings("-A", "not/a/date/at all") + + def test_no_default_ignores_without_ignore_list_ok(self) -> None: + settings = _get_settings("-I") + assert settings.ignore_defaults is False + assert settings.computed.ignore.case is None + assert settings.computed.ignore.ignore_case is None + + def test_lowercase_extra_formats_and_convert_to_accepted(self) -> None: + settings = _get_settings("-x", "zip", "-c", "webp") + assert "ZIP" in settings.formats + assert settings.convert_to is not None + assert "WEBP" in settings.convert_to + + +class _FakeTool(Tool): + """Probe-only tool with a fixed availability.""" + + def __init__(self, name: str, *, available: bool, required: bool = True) -> None: + self.name = name + self.required = required + self._available = available + + @override + def probe_version(self) -> str: + return "1.0" + + @override + def probe(self) -> ToolStatus: + return ToolStatus( + name=self.name, available=self._available, required=self.required + ) + + +class _CountingTool(Tool): + """Tool counting how many real probes run.""" + + name = "counting-tool" + + def __init__(self) -> None: + self.probes = 0 + + @override + def probe_version(self) -> str: + return "1.0" + + @override + def _probe(self) -> ToolStatus: + self.probes += 1 + return ToolStatus(name=self.name, available=True) + + +class TestProbeMemoization: + """probe() runs the real probe once per instance.""" + + def test_probe_is_cached(self: Any) -> None: + tool = _CountingTool() + first = tool.probe() + second = tool.probe() + assert tool.probes == 1 + assert first is second + + +class TestDoctorTierAccounting: + """Tools in a tier are alternatives; one available tool satisfies it.""" + + def test_tier_with_one_available_alternative_is_healthy(self: Any) -> None: + doctor = PicoptDoctor() + tier = ( + _FakeTool("missing-tool", available=False), + _FakeTool("present-tool", available=True), + ) + doctor._checkup_handler_pipeline_tier(0, tier) + assert doctor.total_required == 1 + assert doctor.missing_required == 0 + + def test_tier_with_no_available_tool_is_missing(self: Any) -> None: + doctor = PicoptDoctor() + tier = ( + _FakeTool("gone-a", available=False), + _FakeTool("gone-b", available=False), + ) + doctor._checkup_handler_pipeline_tier(0, tier) + assert doctor.total_required == 1 + assert doctor.missing_required == 1 + + def test_all_optional_tier_never_required(self: Any) -> None: + doctor = PicoptDoctor() + tier = (_FakeTool("extra", available=False, required=False),) + doctor._checkup_handler_pipeline_tier(0, tier) + assert doctor.total_required == 0 + assert doctor.missing_required == 0 + assert doctor.missing_optional == 1 diff --git a/tests/test_convert_container_only.py b/tests/test_convert_container_only.py new file mode 100644 index 0000000..2732427 --- /dev/null +++ b/tests/test_convert_container_only.py @@ -0,0 +1,45 @@ +"""Test container format conversion when no member produces work.""" + +import shutil +from collections.abc import Iterator +from tarfile import TarFile +from zipfile import ZipFile + +import pytest + +from picopt import PROGRAM_NAME, cli +from tests import get_test_dir + +__all__ = () + +TMP_ROOT = get_test_dir() +MEMBER_NAME = "readme.txt" +MEMBER_DATA = b"not an image\n" + + +class TestConvertContainerOnly: + """--convert-to must repack even when every member is a noop.""" + + @pytest.fixture(autouse=True) + def _setup_and_teardown(self) -> Iterator[None]: + shutil.rmtree(TMP_ROOT, ignore_errors=True) + TMP_ROOT.mkdir(parents=True) + yield + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + def test_tar_with_only_noop_members_converts_to_zip(self) -> None: + """A tar with no optimizable members still converts to zip.""" + member_path = TMP_ROOT / MEMBER_NAME + member_path.write_bytes(MEMBER_DATA) + tar_path = TMP_ROOT / "docs.tar" + with TarFile(tar_path, "w") as tf: + tf.add(member_path, MEMBER_NAME) + member_path.unlink() + + cli.main((PROGRAM_NAME, "-rvx", "TAR,ZIP", "-c", "ZIP", str(TMP_ROOT))) + + zip_path = TMP_ROOT / "docs.zip" + assert zip_path.exists() + assert not tar_path.exists() + with ZipFile(zip_path, "r") as zf: + assert zf.read(MEMBER_NAME) == MEMBER_DATA diff --git a/tests/test_dirconfig.py b/tests/test_dirconfig.py new file mode 100644 index 0000000..8e518d8 --- /dev/null +++ b/tests/test_dirconfig.py @@ -0,0 +1,164 @@ +"""Test per-directory .picopt.yaml resolution.""" + +import os +from pathlib import Path + +import pytest + +from picopt import cli +from picopt.config import PicoptConfig +from picopt.config.dirconfig import DirConfig +from picopt.log.summary import Stats + +__all__ = () + + +@pytest.fixture(autouse=True) +def _isolate_config(monkeypatch, tmp_path): + """Point confuse at an empty config dir and scrub picopt env vars.""" + for key in list(os.environ): + if key.startswith("PICOPT"): + monkeypatch.delenv(key, raising=False) + config_dir = tmp_path / "confuse-config" + config_dir.mkdir() + monkeypatch.setenv("PICOPTDIR", str(config_dir)) + + +def _dc(target: Path, *argv: str) -> tuple[DirConfig, Stats]: + """Build a DirConfig the way Walk does.""" + args = cli.get_arguments(("picopt", *argv, str(target))) + picopt_config = PicoptConfig() + global_settings = picopt_config.get_config(args) + stats = Stats() + return DirConfig(PicoptConfig(), args, global_settings, stats), stats + + +def _write(directory: Path, text: str) -> Path: + directory.mkdir(parents=True, exist_ok=True) + config_file = directory / ".picopt.yaml" + config_file.write_text(text) + return config_file + + +class TestDirConfig: + """Per-directory resolution semantics.""" + + def test_no_config_returns_global_instance(self, tmp_path: Path) -> None: + media = tmp_path / "media" + media.mkdir() + dirconfig, _ = _dc(media) + settings = dirconfig.get_settings(media, media) + assert settings is dirconfig._global_settings + + def test_directory_config_overrides_default(self, tmp_path: Path) -> None: + media = tmp_path / "media" + _write(media, "picopt:\n bigger: true\n") + dirconfig, _ = _dc(media) + assert dirconfig._global_settings.bigger is False + assert dirconfig.get_settings(media, media).bigger is True + + def test_deeper_directory_wins(self, tmp_path: Path) -> None: + media = tmp_path / "media" + deep = media / "deep" + _write(media, "picopt:\n near_lossless: true\n") + _write(deep, "picopt:\n near_lossless: false\n") + dirconfig, _ = _dc(media) + assert dirconfig.get_settings(media, media).near_lossless is True + assert dirconfig.get_settings(media, deep).near_lossless is False + + def test_boundary_stops_at_top_path(self, tmp_path: Path) -> None: + _write(tmp_path, "picopt:\n bigger: true\n") + media = tmp_path / "media" + media.mkdir() + dirconfig, _ = _dc(media) + settings = dirconfig.get_settings(media, media) + assert settings.bigger is False + + def test_cli_wins_over_directory_config(self, tmp_path: Path) -> None: + media = tmp_path / "media" + _write(media, "picopt:\n keep_metadata: true\n") + dirconfig, _ = _dc(media, "-M") + assert dirconfig.get_settings(media, media).keep_metadata is False + + def test_env_wins_over_directory_config(self, tmp_path: Path, monkeypatch) -> None: + media = tmp_path / "media" + _write(media, "picopt:\n bigger: false\n") + monkeypatch.setenv("PICOPT_PICOPT__BIGGER", "True") + dirconfig, _ = _dc(media) + assert dirconfig.get_settings(media, media).bigger is True + + def test_formats_replace_not_union(self, tmp_path: Path) -> None: + media = tmp_path / "media" + _write(media, "picopt:\n formats: [CBZ]\n") + dirconfig, _ = _dc(media) + settings = dirconfig.get_settings(media, media) + assert settings.formats == ("CBZ",) + + def test_full_schema_convert_to(self, tmp_path: Path) -> None: + media = tmp_path / "media" + _write(media, "picopt:\n convert_to: [WEBP]\n") + dirconfig, _ = _dc(media) + settings = dirconfig.get_settings(media, media) + assert settings.convert_to is not None + assert "WEBP" in settings.convert_to + + def test_single_file_target_boundary(self, tmp_path: Path) -> None: + media = tmp_path / "media" + _write(media, "picopt:\n bigger: true\n") + _write(tmp_path, "picopt:\n near_lossless: true\n") + target_file = media / "img.png" + target_file.write_bytes(b"x") + dirconfig, _ = _dc(target_file) + settings = dirconfig.get_settings(media, media) + assert settings.bigger is True + # The config above the target boundary must not apply. + assert settings.near_lossless is False + + def test_malformed_config_falls_back_and_records_error( + self, tmp_path: Path + ) -> None: + media = tmp_path / "media" + _write(media / "bad", "picopt: [not: a: mapping\n") + _write(media / "good", "picopt:\n bigger: true\n") + dirconfig, stats = _dc(media) + bad_settings = dirconfig.get_settings(media, media / "bad") + assert bad_settings is dirconfig._global_settings + assert stats.errors + # A sibling with a valid config still resolves. + assert dirconfig.get_settings(media, media / "good").bigger is True + + def test_malformed_config_logged_once(self, tmp_path: Path) -> None: + media = tmp_path / "media" + _write(media, "picopt: [not: a: mapping\n") + child_a = media / "a" + child_b = media / "b" + child_a.mkdir() + child_b.mkdir() + dirconfig, stats = _dc(media) + dirconfig.get_settings(media, child_a) + dirconfig.get_settings(media, child_b) + assert len(stats.errors) == 1 + + def test_settings_cached_per_directory(self, tmp_path: Path) -> None: + media = tmp_path / "media" + _write(media, "picopt:\n bigger: true\n") + dirconfig, _ = _dc(media) + first = dirconfig.get_settings(media, media) + second = dirconfig.get_settings(media, media) + assert first is second + + def test_run_mode_keys_forced_run_level(self, tmp_path: Path) -> None: + media = tmp_path / "media" + _write( + media, + "picopt:\n dry_run: true\n list_only: true\n timestamps: true\n" + " bigger: true\n", + ) + dirconfig, _ = _dc(media) + settings = dirconfig.get_settings(media, media) + # The per-directory value applies for normal keys... + assert settings.bigger is True + # ...but run-mode keys stay pinned to the run-level values. + assert settings.dry_run is False + assert settings.list_only is False + assert settings.timestamps is False diff --git a/tests/test_dirconfig_walk.py b/tests/test_dirconfig_walk.py new file mode 100644 index 0000000..99b4f20 --- /dev/null +++ b/tests/test_dirconfig_walk.py @@ -0,0 +1,136 @@ +"""End-to-end tests for per-directory .picopt.yaml in the walk.""" + +import os +import shutil +from collections.abc import Iterator +from pathlib import Path +from zipfile import ZipFile + +import pytest + +from picopt import PROGRAM_NAME, cli +from tests import CONTAINER_DIR, IMAGES_DIR, get_test_dir + +__all__ = () + +TMP_ROOT = get_test_dir() +PNG_FN = "test_png.png" +CBR_FN = "test_cbr.cbr" +CBZ_FN = "test_cbz.cbz" + + +@pytest.fixture(autouse=True) +def _isolate(monkeypatch, tmp_path) -> Iterator[None]: + """Scrub env, isolate the user config, and build a fresh tree.""" + for key in list(os.environ): + if key.startswith("PICOPT"): + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("PICOPTDIR", str(tmp_path)) + shutil.rmtree(TMP_ROOT, ignore_errors=True) + TMP_ROOT.mkdir(parents=True) + yield + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + +def _write_dir_config(directory: Path, text: str) -> None: + directory.mkdir(parents=True, exist_ok=True) + (directory / ".picopt.yaml").write_text(text) + + +class TestDirConfigWalk: + """Directory configs steer the walk, handlers, and conversions.""" + + def test_convert_to_applies_only_in_configured_subtree(self) -> None: + comics = TMP_ROOT / "comics" + _write_dir_config(comics, "picopt:\n convert_to: [CBZ]\n") + shutil.copy(CONTAINER_DIR / CBR_FN, comics / CBR_FN) + shutil.copy(CONTAINER_DIR / CBR_FN, TMP_ROOT / CBR_FN) + + cli.main((PROGRAM_NAME, "-rvx", "CBR,CBZ", str(TMP_ROOT))) + + # Converted under comics/ ... + assert (comics / "test_cbr.cbz").is_file() + assert not (comics / CBR_FN).exists() + # ... untouched at the root (no convert target there). + assert (TMP_ROOT / CBR_FN).is_file() + + def test_recurse_false_subtree_untouched(self, tmp_path: Path) -> None: + # recurse comes from the user config so the dir config can override + # it — an explicit CLI -r would (correctly) win over both. + (tmp_path / "config.yaml").write_text("picopt:\n recurse: true\n") + frozen = TMP_ROOT / "frozen" + _write_dir_config(frozen, "picopt:\n recurse: false\n") + shutil.copy(IMAGES_DIR / PNG_FN, frozen / PNG_FN) + shutil.copy(IMAGES_DIR / PNG_FN, TMP_ROOT / PNG_FN) + frozen_size = (frozen / PNG_FN).stat().st_size + + cli.main((PROGRAM_NAME, "-vx", "PNG", str(TMP_ROOT))) + + assert (frozen / PNG_FN).stat().st_size == frozen_size + assert (TMP_ROOT / PNG_FN).stat().st_size < frozen_size + + def test_cli_recurse_beats_dir_config(self) -> None: + frozen = TMP_ROOT / "frozen" + _write_dir_config(frozen, "picopt:\n recurse: false\n") + shutil.copy(IMAGES_DIR / PNG_FN, frozen / PNG_FN) + orig_size = (frozen / PNG_FN).stat().st_size + + cli.main((PROGRAM_NAME, "-rvx", "PNG", str(TMP_ROOT))) + + assert (frozen / PNG_FN).stat().st_size < orig_size + + def test_per_dir_ignore(self) -> None: + photos = TMP_ROOT / "photos" + _write_dir_config(photos, "picopt:\n ignore: ['*skipme*']\n") + shutil.copy(IMAGES_DIR / PNG_FN, photos / "skipme.png") + shutil.copy(IMAGES_DIR / PNG_FN, photos / "doit.png") + orig_size = (photos / "skipme.png").stat().st_size + + cli.main((PROGRAM_NAME, "-rvx", "PNG", str(TMP_ROOT))) + + assert (photos / "skipme.png").stat().st_size == orig_size + assert (photos / "doit.png").stat().st_size < orig_size + + def test_dir_config_edit_invalidates_timestamps(self) -> None: + photos = TMP_ROOT / "photos" + _write_dir_config(photos, "picopt:\n bigger: false\n") + shutil.copy(IMAGES_DIR / PNG_FN, photos / PNG_FN) + argv = (PROGRAM_NAME, "-rtvx", "PNG", str(TMP_ROOT)) + + cli.main(argv) + optimized_mtime = (photos / PNG_FN).stat().st_mtime_ns + + # Second run: stamped, untouched. + cli.main(argv) + assert (photos / PNG_FN).stat().st_mtime_ns == optimized_mtime + + # Edit the directory config: the fingerprint flips and the tree + # re-processes (the already-optimized file is re-examined; its + # replacement attempt is discarded but it is no longer skipped + # by timestamp, so it gets a fresh stamp pass). + _write_dir_config(photos, "picopt:\n bigger: true\n") + cli.main(argv) + # With bigger=true the re-optimized (equal or larger) result is + # kept, so the file's mtime changes — proof it was re-processed. + assert (photos / PNG_FN).stat().st_mtime_ns != optimized_mtime + + def test_archive_members_inherit_dir_config(self) -> None: + comics = TMP_ROOT / "comics" + _write_dir_config(comics, "picopt:\n convert_to: [WEBP]\n") + comics_cbz = comics / "pngs.cbz" + root_cbz = TMP_ROOT / "pngs.cbz" + # A CBZ with a losslessly-convertible (PNG) member. + with ZipFile(comics_cbz, "w") as zf: + zf.write(IMAGES_DIR / PNG_FN, PNG_FN) + shutil.copy(comics_cbz, root_cbz) + + cli.main((PROGRAM_NAME, "-rvx", "CBZ,PNG,WEBP", str(TMP_ROOT))) + + with ZipFile(comics_cbz) as zf: + comics_suffixes = {Path(n).suffix for n in zf.namelist()} + with ZipFile(root_cbz) as zf: + root_suffixes = {Path(n).suffix for n in zf.namelist()} + # Members under comics/ converted to webp; the root archive kept + # its original member formats. + assert comics_suffixes == {".webp"} + assert root_suffixes == {".png"} diff --git a/tests/test_idempotency.py b/tests/test_idempotency.py new file mode 100644 index 0000000..9faf96c --- /dev/null +++ b/tests/test_idempotency.py @@ -0,0 +1,93 @@ +"""Property tests: optimizing twice is a noop; lossless paths keep pixels.""" + +import shutil +from collections.abc import Iterator +from pathlib import Path + +import pytest +from PIL import Image + +from picopt import PROGRAM_NAME, cli +from tests import CONTAINER_DIR, IMAGES_DIR, get_test_dir + +__all__ = () + +TMP_ROOT = get_test_dir() +IMAGE_FNS = ( + "test_png.png", + "test_jpg.jpg", + "test_gif.gif", + "test_webp_lossless.webp", +) +CBZ_FN = "test_cbz.cbz" +FORMATS_ARG = "PNG,JPEG,GIF,WEBP,CBZ" + + +def _pixels(path: Path) -> bytes: + with Image.open(path) as image: + return image.convert("RGBA").tobytes() + + +@pytest.fixture(autouse=True) +def _setup_and_teardown() -> Iterator[None]: + shutil.rmtree(TMP_ROOT, ignore_errors=True) + TMP_ROOT.mkdir(parents=True) + yield + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + +class TestIdempotency: + """Repeated optimization converges: never grows, fixed by the 2nd pass.""" + + def test_optimization_converges(self) -> None: + for fn in IMAGE_FNS: + shutil.copy(IMAGES_DIR / fn, TMP_ROOT / fn) + shutil.copy(CONTAINER_DIR / CBZ_FN, TMP_ROOT / CBZ_FN) + argv = (PROGRAM_NAME, "-rvx", FORMATS_ARG, str(TMP_ROOT)) + all_fns = (*IMAGE_FNS, CBZ_FN) + + cli.main(argv) + first = {fn: (TMP_ROOT / fn).read_bytes() for fn in all_fns} + + # A second pass may squeeze a few more bytes out of a multi-tool + # pipeline (one tool's output enables another), but never grow. + cli.main(argv) + second = {fn: (TMP_ROOT / fn).read_bytes() for fn in all_fns} + for fn in all_fns: + assert len(second[fn]) <= len(first[fn]), fn + + # By the third pass every file must be at its fixed point. + cli.main(argv) + for fn in all_fns: + assert (TMP_ROOT / fn).read_bytes() == second[fn], fn + + +class TestPixelIdentity: + """Lossless optimization and conversion must not change a single pixel.""" + + def test_png_optimize_keeps_pixels(self) -> None: + target = TMP_ROOT / "test_png.png" + shutil.copy(IMAGES_DIR / "test_png.png", target) + before = _pixels(target) + cli.main((PROGRAM_NAME, "-rvx", "PNG", str(TMP_ROOT))) + assert target.stat().st_size < (IMAGES_DIR / "test_png.png").stat().st_size + assert _pixels(target) == before + + def test_png_to_webp_conversion_keeps_pixels(self) -> None: + source = TMP_ROOT / "test_png.png" + shutil.copy(IMAGES_DIR / "test_png.png", source) + before = _pixels(source) + cli.main((PROGRAM_NAME, "-rvx", "PNG,WEBP", "-c", "WEBP", str(TMP_ROOT))) + converted = TMP_ROOT / "test_png.webp" + assert converted.is_file() + assert not source.exists() + assert _pixels(converted) == before + + def test_gif_to_png_conversion_keeps_pixels(self) -> None: + source = TMP_ROOT / "test_gif.gif" + shutil.copy(IMAGES_DIR / "test_gif.gif", source) + before = _pixels(source) + cli.main((PROGRAM_NAME, "-rvx", "GIF,PNG", "-c", "PNG", str(TMP_ROOT))) + converted = TMP_ROOT / "test_gif.png" + assert converted.is_file() + assert _pixels(converted) == before diff --git a/tests/test_nested_archives.py b/tests/test_nested_archives.py new file mode 100644 index 0000000..e2f9939 --- /dev/null +++ b/tests/test_nested_archives.py @@ -0,0 +1,72 @@ +"""Test optimizing archives nested inside other archives.""" + +import shutil +from collections.abc import Iterator +from io import BytesIO +from tarfile import TarFile +from zipfile import ZIP_STORED, ZipFile + +import pytest + +from picopt import PROGRAM_NAME, cli +from tests import CONTAINER_DIR, get_test_dir + +__all__ = () + +TMP_ROOT = get_test_dir() +SRC_CBZ = CONTAINER_DIR / "test_cbz.cbz" +INNER_NAME = "inner.cbz" +OUTER_NAME = "outer.zip" +OUTER_TAR_NAME = "outer.tar" +ARGS = (PROGRAM_NAME, "-rvvtx", "ZIP,CBZ,TAR") + + +class TestNestedArchives: + """Nested archives must unpack, optimize, and repack.""" + + @pytest.fixture(autouse=True) + def _setup_and_teardown(self) -> Iterator[None]: + shutil.rmtree(TMP_ROOT, ignore_errors=True) + TMP_ROOT.mkdir(parents=True) + yield + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + def test_cbz_nested_in_zip(self) -> None: + """A CBZ inside a ZIP is optimized in place.""" + outer_path = TMP_ROOT / OUTER_NAME + with ZipFile(outer_path, "w", compression=ZIP_STORED) as zf: + zf.write(SRC_CBZ, INNER_NAME) + orig_outer_size = outer_path.stat().st_size + + cli.main((*ARGS, str(TMP_ROOT))) + + assert outer_path.stat().st_size < orig_outer_size + with ZipFile(outer_path, "r") as zf: + assert zf.namelist() == [INNER_NAME] + inner_bytes = zf.read(INNER_NAME) + assert len(inner_bytes) < SRC_CBZ.stat().st_size + # The optimized inner member must still be a readable zip. + with ZipFile(BytesIO(inner_bytes), "r") as inner_zf: + assert inner_zf.testzip() is None + assert inner_zf.namelist() + + def test_cbz_nested_in_tar(self) -> None: + """A CBZ inside an uncompressed tar is optimized in place.""" + outer_path = TMP_ROOT / OUTER_TAR_NAME + with TarFile(outer_path, "w") as tf: + tf.add(SRC_CBZ, INNER_NAME) + orig_outer_size = outer_path.stat().st_size + + cli.main((*ARGS, str(TMP_ROOT))) + + assert outer_path.stat().st_size < orig_outer_size + with TarFile(outer_path, "r") as tf: + names = tf.getnames() + assert INNER_NAME in names + member = tf.extractfile(INNER_NAME) + assert member is not None + inner_bytes = member.read() + assert len(inner_bytes) < SRC_CBZ.stat().st_size + with ZipFile(BytesIO(inner_bytes), "r") as inner_zf: + assert inner_zf.testzip() is None + assert inner_zf.namelist() diff --git a/tests/test_pdf_encryption.py b/tests/test_pdf_encryption.py new file mode 100644 index 0000000..d728bf2 --- /dev/null +++ b/tests/test_pdf_encryption.py @@ -0,0 +1,52 @@ +"""Test that repacking preserves PDF encryption for restricted files.""" + +import shutil +from collections.abc import Iterator +from io import BytesIO + +import pikepdf +import pytest + +from picopt import cli +from picopt.config import PicoptConfig +from picopt.path import PathInfo +from picopt.plugins.pdf import Pdf +from tests import IMAGES_DIR, get_test_dir + +__all__ = () + +TMP_ROOT = get_test_dir() +SRC_PDF = IMAGES_DIR / "07themecamplist.pdf" +OWNER_PASSWORD = "owner-secret" # noqa: S105 + + +class TestPdfEncryptionPreserved: + """Owner-password-restricted PDFs must not be silently decrypted.""" + + @pytest.fixture(autouse=True) + def _setup_and_teardown(self) -> Iterator[None]: + shutil.rmtree(TMP_ROOT, ignore_errors=True) + TMP_ROOT.mkdir(parents=True) + yield + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + def test_pack_into_keeps_owner_password_encryption(self) -> None: + restricted_path = TMP_ROOT / "restricted.pdf" + with pikepdf.open(SRC_PDF) as pdf: + pdf.save( + restricted_path, + encryption=pikepdf.Encryption(owner=OWNER_PASSWORD, user=""), + ) + # Opens without a password: only the owner permissions are locked. + with pikepdf.open(restricted_path) as pdf: + assert pdf.is_encrypted + + config = PicoptConfig().get_config( + cli.get_arguments(("picopt", "-x", "PDF", str(TMP_ROOT))) + ) + path_info = PathInfo(top_path=TMP_ROOT, path=restricted_path, convert=False) + handler = Pdf(config, path_info, input_file_format=Pdf.OUTPUT_FILE_FORMAT) + output_buffer = handler.pack_into() + + with pikepdf.open(BytesIO(output_buffer.getvalue())) as repacked: + assert repacked.is_encrypted diff --git a/tests/test_scheduler_errors.py b/tests/test_scheduler_errors.py new file mode 100644 index 0000000..7683d21 --- /dev/null +++ b/tests/test_scheduler_errors.py @@ -0,0 +1,342 @@ +"""Test that scheduler failure paths never drop archive members.""" + +from pathlib import Path +from typing import Any + +from picopt import cli +from picopt.config import PicoptConfig +from picopt.report import ReportStats +from picopt.walk.scheduler import OptimizeLeafJob, Scheduler, _LeafEntry + +__all__ = () # hides module from pydocstring + +_MEMBER_PATH = Path("member.png") +_ARCHIVE_PATH = Path("test.cbz") + + +class _FakePathInfo: + """Minimal PathInfo stand-in for error-path bookkeeping.""" + + def __init__( + self, name: str, path: Path | None = None, top_path: Path | None = None + ) -> None: + self._name = name + self.path: Path | None = path + self.top_path: Path | None = top_path + + def bytes_in(self) -> int: + return 100 + + +class _FakeContainerHandler: + """Minimal ContainerHandler stand-in for completion handling.""" + + def __init__(self, name: str) -> None: + self.path_info = _FakePathInfo(name) + self.original_path = Path(name) + self.repack_handler_class = None + self.hydrate_calls: list[tuple[Any, Any]] = [] + self.staging_dir: Path | None = None + # The scheduler passes this to create_repack_handler. + self.config = None + self._optimized_contents: set[Any] = set() + self._do_repack = False + + def get_staging_dir(self) -> Path | None: + return self.staging_dir + + def get_optimized_contents(self) -> set[Any]: + return self._optimized_contents + + def is_do_repack(self) -> bool: + return self._do_repack + + def set_do_repack(self, *, do_repack: bool) -> None: + self._do_repack = do_repack + + def hydrate_optimized_path_info(self, path_info: Any, report: Any) -> None: + self.hydrate_calls.append((path_info, report)) + + +class _FakeReporter: + """Records every report it is handed.""" + + def __init__(self) -> None: + self.reports: list[ReportStats] = [] + + def record_report(self, report: ReportStats) -> None: + self.reports.append(report) + + +class _FakeTimestamps: + """Records every timestamp set() call.""" + + def __init__(self) -> None: + self.set_calls: list[tuple] = [] + + def set(self, *args, **kwargs) -> None: + self.set_calls.append((args, kwargs)) + + +def _make_scheduler( + *argv: str, +) -> tuple[Scheduler, _FakeReporter, _FakeTimestamps]: + args = cli.get_arguments(("picopt", *argv, ".")) + config = PicoptConfig().get_config(args) + reporter = _FakeReporter() + timestamps = _FakeTimestamps() + scheduler = Scheduler( + config=config, + executor=None, # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + timestamps=timestamps, # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + reporter=reporter, # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + max_workers=1000, + create_repack_handler=lambda _config, handler: handler, + child_enqueue_callback=lambda *_a: None, + ) + return scheduler, reporter, timestamps + + +class TestSchedulerErrorPaths: + """Errored members must be preserved in their parent container.""" + + def test_errored_leaf_kept_in_parent_contents(self: Any) -> None: + """A member whose optimization errors stays in the repacked archive.""" + scheduler, reporter, _ = _make_scheduler() + parent_handler = _FakeContainerHandler(str(_ARCHIVE_PATH)) + parent = scheduler.enqueue_container(parent_handler) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + + member_info = _FakePathInfo(str(_MEMBER_PATH)) + job = OptimizeLeafJob(handler=None, path_info=member_info) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + scheduler.enqueue_leaf(job, parent) + assert parent.pending == 1 + + report = ReportStats(_MEMBER_PATH, exc=ValueError("simulated failure")) + entry = _LeafEntry(job=job, parent=parent) + scheduler._handle_leaf_done(entry, report) + + assert member_info in parent_handler.get_optimized_contents() + assert parent.pending == 0 + assert parent.had_error + assert any(r.exc for r in reporter.reports) + + def test_failed_nested_repack_kept_in_parent_contents(self: Any) -> None: + """A nested container whose repack fails stays in the parent archive.""" + scheduler, reporter, _ = _make_scheduler() + parent_handler = _FakeContainerHandler(str(_ARCHIVE_PATH)) + parent = scheduler.enqueue_container(parent_handler) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + + child_handler = _FakeContainerHandler("nested.cbz") + child = scheduler.enqueue_container(child_handler, parent) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + assert parent.pending == 1 + + report = ReportStats(Path("nested.cbz"), exc=ValueError("repack failed")) + scheduler._handle_repack_done(child, report) + + assert child_handler.path_info in parent_handler.get_optimized_contents() + assert parent.pending == 0 + assert parent.had_error + assert any(r.exc for r in reporter.reports) + + +class TestDirTimestampsOnError: + """Directories and containers with errored children must not be stamped.""" + + def test_errored_leaf_poisons_dir_timestamp(self: Any, tmp_path: Path) -> None: + """A failed file blocks the compacted dir stamp; successes don't.""" + scheduler, _, timestamps = _make_scheduler() + scheduler.begin_dir(tmp_path, tmp_path) + + member_path = tmp_path / "bad.png" + member_info = _FakePathInfo(str(member_path), path=member_path) + job = OptimizeLeafJob(handler=None, path_info=member_info) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + scheduler.enqueue_leaf(job) + + report = ReportStats(member_path, exc=ValueError("boom")) + scheduler._handle_leaf_done(_LeafEntry(job=job, parent=None), report) + scheduler.seal_dir(tmp_path) + + assert tmp_path not in scheduler._dirs + assert not timestamps.set_calls + + def test_clean_dir_still_gets_compacted_timestamp( + self: Any, tmp_path: Path + ) -> None: + """Without errors the dir stamp is written with compaction.""" + scheduler, _, timestamps = _make_scheduler() + scheduler.begin_dir(tmp_path, tmp_path) + + member_path = tmp_path / "good.png" + member_info = _FakePathInfo(str(member_path), path=member_path) + job = OptimizeLeafJob(handler=None, path_info=member_info) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + scheduler.enqueue_leaf(job) + + report = ReportStats(member_path, bytes_in=10, bytes_out=5, changed=True) + scheduler._handle_leaf_done(_LeafEntry(job=job, parent=None), report) + scheduler.seal_dir(tmp_path) + + assert tmp_path not in scheduler._dirs + compacted = [c for c in timestamps.set_calls if c[1].get("compact")] + assert compacted + + def test_failed_top_level_container_notifies_dir_tracker( + self: Any, tmp_path: Path + ) -> None: + """A failed container repack must not leak its directory tracker.""" + scheduler, _, timestamps = _make_scheduler() + scheduler.begin_dir(tmp_path, tmp_path) + + archive_path = tmp_path / "broken.cbz" + handler = _FakeContainerHandler(str(archive_path)) + handler.path_info.path = archive_path + node = scheduler.enqueue_container(handler) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + + report = ReportStats(archive_path, exc=ValueError("repack failed")) + scheduler._handle_repack_done(node, report) + scheduler.seal_dir(tmp_path) + + assert tmp_path not in scheduler._dirs + assert not timestamps.set_calls + + def test_container_with_errored_member_not_timestamped( + self: Any, tmp_path: Path + ) -> None: + """A container whose member errored gets no timestamp on success.""" + scheduler, _, timestamps = _make_scheduler() + archive_path = tmp_path / "partial.cbz" + handler = _FakeContainerHandler(str(archive_path)) + handler.path_info.path = archive_path + handler.path_info.top_path = tmp_path + node = scheduler.enqueue_container(handler) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + node.had_error = True + + report = ReportStats(archive_path, bytes_in=10, bytes_out=5, changed=True) + scheduler._handle_repack_done(node, report) + + assert not timestamps.set_calls + + +class TestStagingCleanup: + """Worker staging dirs must be cleaned on rollback, not leaked.""" + + def test_unpack_wires_staging_dir_onto_node(self: Any, tmp_path: Path) -> None: + """The scheduler learns the staging dir from the unpacked handler.""" + from picopt.walk.scheduler import UnpackResult + + scheduler, _, _ = _make_scheduler() + handler = _FakeContainerHandler(str(tmp_path / "anim.webp")) + node = scheduler.enqueue_container(handler) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + + staging = tmp_path / "staging" + staging.mkdir() + handler.staging_dir = staging + scheduler._handle_unpack_done( + node, + UnpackResult(handler=handler, children=[]), # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + ) + # The noop completion retires the node and cleans its staging. + assert not staging.exists() + + def test_cancelled_subtree_cleans_staging(self: Any, tmp_path: Path) -> None: + """Rollback removes the worker's staging dir from disk.""" + scheduler, _, _ = _make_scheduler() + handler = _FakeContainerHandler(str(tmp_path / "broken.cbz")) + node = scheduler.enqueue_container(handler) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + staging = tmp_path / "staging" + staging.mkdir() + (staging / "frame_0.webp").write_bytes(b"x") + node.staging_dir = staging + + scheduler._cancel_subtree(node, reason=None) + + assert not staging.exists() + + +class TestNestedConversionRename: + """Nested container repacks must hydrate renames onto the parent.""" + + def test_nested_repack_success_hydrates_parent(self: Any) -> None: + """The parent handler hydrates data and rename from the report.""" + scheduler, _, _ = _make_scheduler() + parent_handler = _FakeContainerHandler(str(_ARCHIVE_PATH)) + parent = scheduler.enqueue_container(parent_handler) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + child_handler = _FakeContainerHandler("inner.cbr") + child = scheduler.enqueue_container(child_handler, parent) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + + report = ReportStats( + Path("inner.cbz"), bytes_in=10, bytes_out=5, changed=True, data=b"zipdata" + ) + scheduler._handle_repack_done(child, report) + + assert parent_handler.hydrate_calls == [(child_handler.path_info, report)] + assert child_handler.path_info in parent_handler.get_optimized_contents() + assert parent.had_work + + +class TestFailFast: + """--fail-fast cancels everything on the first error.""" + + def test_fail_fast_cancels_all_live_tops_and_clears_queues(self: Any) -> None: + """One repack failure cancels every live top-level container.""" + from picopt.walk.scheduler import NodeState + + scheduler, reporter, timestamps = _make_scheduler("--fail-fast") + handler_a = _FakeContainerHandler("a.cbz") + handler_b = _FakeContainerHandler("b.cbz") + node_a = scheduler.enqueue_container(handler_a) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + node_b = scheduler.enqueue_container(handler_b) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + + report = ReportStats(Path("a.cbz"), exc=ValueError("boom")) + scheduler._handle_repack_done(node_a, report) + + assert node_a.state is NodeState.CANCELLED + assert node_b.state is NodeState.CANCELLED + assert not scheduler._ready + assert not scheduler._gated + assert any(r.exc for r in reporter.reports) + assert not timestamps.set_calls + + def test_fail_fast_container_escalates_to_subtree_root_only(self: Any) -> None: + """An inner repack failure cancels its top container, not siblings.""" + from picopt.walk.scheduler import NodeState + + scheduler, reporter, _ = _make_scheduler("--fail-fast-container") + top_a = _FakeContainerHandler("a.cbz") + node_a = scheduler.enqueue_container(top_a) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + inner = _FakeContainerHandler("a_inner.cbz") + node_inner = scheduler.enqueue_container(inner, node_a) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + top_b = _FakeContainerHandler("b.cbz") + node_b = scheduler.enqueue_container(top_b) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + + report = ReportStats(Path("a_inner.cbz"), exc=ValueError("inner boom")) + scheduler._handle_repack_done(node_inner, report) + + assert node_inner.state is NodeState.CANCELLED + assert node_a.state is NodeState.CANCELLED + # The sibling top-level container is untouched. + assert node_b.state is not NodeState.CANCELLED + assert any(r.exc for r in reporter.reports) + + def test_late_leaf_result_for_cancelled_parent_is_dropped(self: Any) -> None: + """A leaf completing after its parent cancelled leaves no residue.""" + from picopt.walk.scheduler import NodeState + + scheduler, _, _ = _make_scheduler() + parent_handler = _FakeContainerHandler("late.cbz") + parent = scheduler.enqueue_container(parent_handler) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + member_info = _FakePathInfo("member.png") + job = OptimizeLeafJob(handler=None, path_info=member_info) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + scheduler.enqueue_leaf(job, parent) + + scheduler._cancel_subtree(parent, reason=None) + assert parent.state is NodeState.CANCELLED + + report = ReportStats( + Path("member.png"), bytes_in=10, bytes_out=5, changed=True, data=b"x" + ) + scheduler._handle_leaf_done(_LeafEntry(job=job, parent=parent), report) + + # Dropped on the floor: no contents, no hydration, pending drained. + assert member_info not in parent_handler.get_optimized_contents() + assert not parent_handler.hydrate_calls + assert parent.pending == 0 diff --git a/tests/test_scheduler_memory.py b/tests/test_scheduler_memory.py new file mode 100644 index 0000000..a5fd880 --- /dev/null +++ b/tests/test_scheduler_memory.py @@ -0,0 +1,118 @@ +"""Test the scheduler's memory-aware admission gate.""" + +from typing import Any + +from picopt import cli +from picopt.config import PicoptConfig +from picopt.walk.scheduler import _MEM_COST_FACTOR, Scheduler + +__all__ = () # hides module from pydocstring + +# Each container of on-disk size S is charged S * _MEM_COST_FACTOR. +_SIZE = 100 +_COST = _SIZE * _MEM_COST_FACTOR +_FITS = 2 # how many such containers fit the budget together +_BUDGET = _COST * _FITS # budget sized to admit exactly _FITS at once +_TOTAL = 3 # containers enqueued +_DEFERRED = _TOTAL - _FITS +_ALL = 5 +_NONE = 0 +_BIG_SIZE = _BUDGET # one container whose cost far exceeds the whole budget + + +class _FakePathInfo: + """Minimal PathInfo stand-in exposing only what the gate reads.""" + + def __init__(self, size: int) -> None: + self._size = size + self.path = None + + def bytes_in(self) -> int: + return self._size + + +class _FakeHandler: + """Minimal ContainerHandler stand-in with a sized path_info.""" + + def __init__(self, size: int) -> None: + self.path_info = _FakePathInfo(size) + + +class _StubExecutor: + """Records submissions and returns opaque futures that never run.""" + + def __init__(self) -> None: + self.submitted: list[Any] = [] + + def submit(self, fn: Any) -> object: + self.submitted.append(fn) + return object() + + +def _make_scheduler(memory_limit_bytes: int) -> "tuple[Scheduler, _StubExecutor]": + args = cli.get_arguments(("picopt", "--memory-limit", str(memory_limit_bytes), ".")) + config = PicoptConfig().get_config(args) + executor = _StubExecutor() + scheduler = Scheduler( + config=config, + executor=executor, # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + timestamps=None, + reporter=None, # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + max_workers=1000, # keep the 2*max_workers future cap out of the way + create_repack_handler=lambda *_a: None, # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + child_enqueue_callback=lambda *_a: None, + ) + return scheduler, executor + + +class TestSchedulerMemoryGate: + """Test memory-budget admission control.""" + + def test_gate_admits_within_budget(self: Any) -> None: + """Only as many large containers as fit the budget start at once.""" + scheduler, executor = _make_scheduler(_BUDGET) + for _ in range(_TOTAL): + scheduler.enqueue_container(_FakeHandler(_SIZE)) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + scheduler._submit_ready() + assert len(executor.submitted) == _FITS + assert scheduler._inflight_bytes == _COST * _FITS + assert len(scheduler._gated) == _DEFERRED + assert len(scheduler._ready) == _NONE + + def test_gate_runs_oversized_item_alone(self: Any) -> None: + """A single item bigger than the whole budget still runs (alone).""" + scheduler, executor = _make_scheduler(_BUDGET) + scheduler.enqueue_container(_FakeHandler(_BIG_SIZE)) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + scheduler._submit_ready() + assert len(executor.submitted) == 1 + assert scheduler._inflight_bytes == _BIG_SIZE * _MEM_COST_FACTOR + + def test_retiring_a_node_frees_budget(self: Any) -> None: + """A deferred item is admitted once an in-flight node retires.""" + scheduler, executor = _make_scheduler(_BUDGET) + nodes = [ + scheduler.enqueue_container(_FakeHandler(_SIZE)) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + for _ in range(_TOTAL) + ] + scheduler._submit_ready() + assert len(scheduler._gated) == _DEFERRED + assert len(scheduler._ready) == _NONE + + admitted = [node for node in nodes if node.cost > 0] + scheduler._retire_node(admitted[0]) + assert scheduler._inflight_bytes == _COST * (_FITS - 1) + + scheduler._submit_ready() + assert len(executor.submitted) == _TOTAL + assert len(scheduler._ready) == _NONE + assert len(scheduler._gated) == _NONE + + def test_large_budget_admits_everything(self: Any) -> None: + """A generous budget never defers work.""" + scheduler, executor = _make_scheduler(1024**4) # 1 TiB + for _ in range(_ALL): + scheduler.enqueue_container(_FakeHandler(_SIZE)) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + scheduler._submit_ready() + assert len(executor.submitted) == _ALL + assert len(scheduler._ready) == _NONE + assert len(scheduler._gated) == _NONE diff --git a/tests/test_svg_lossless.py b/tests/test_svg_lossless.py new file mode 100644 index 0000000..cd1ea4e --- /dev/null +++ b/tests/test_svg_lossless.py @@ -0,0 +1,63 @@ +"""Test that svgo runs with a lossless, metadata-respecting configuration.""" + +import shutil +from collections.abc import Iterator + +import pytest + +from picopt import PROGRAM_NAME, cli +from tests import get_test_dir + +__all__ = () + +TMP_ROOT = get_test_dir() +SVG_FN = "boxed.svg" +# Enough repetitive filler that svgo produces a smaller file and picopt +# actually replaces it. +_SVG_RECT = ( + ' \n' +) +SVG_SOURCE = "".join( + ( + '\n', + " Keep Me\n", + " Important description\n", + *(_SVG_RECT.format(i=i) for i in range(20)), + "\n", + ) +) + + +class TestSvgLossless: + """viewBox and metadata must survive optimization.""" + + @pytest.fixture(autouse=True) + def _setup_and_teardown(self) -> Iterator[None]: + shutil.rmtree(TMP_ROOT, ignore_errors=True) + TMP_ROOT.mkdir(parents=True) + yield + shutil.rmtree(TMP_ROOT, ignore_errors=True) + + def test_viewbox_and_metadata_preserved(self) -> None: + svg_path = TMP_ROOT / SVG_FN + svg_path.write_text(SVG_SOURCE) + orig_size = svg_path.stat().st_size + + cli.main((PROGRAM_NAME, "-rvx", "SVG", str(TMP_ROOT))) + + optimized = svg_path.read_text() + assert svg_path.stat().st_size < orig_size + assert "viewBox" in optimized + assert "Keep Me" in optimized + assert "Important description" in optimized + + def test_strip_metadata_still_keeps_viewbox(self) -> None: + svg_path = TMP_ROOT / SVG_FN + svg_path.write_text(SVG_SOURCE) + + cli.main((PROGRAM_NAME, "-rvMx", "SVG", str(TMP_ROOT))) + + optimized = svg_path.read_text() + assert "viewBox" in optimized + assert "Keep Me" not in optimized diff --git a/tests/test_timestamps.py b/tests/test_timestamps.py index 40ada4d..fff3b1f 100644 --- a/tests/test_timestamps.py +++ b/tests/test_timestamps.py @@ -2,6 +2,7 @@ import shutil from datetime import datetime, timezone +from hashlib import sha256 from pathlib import Path from types import MappingProxyType @@ -32,6 +33,9 @@ "near_lossless": False, "recurse": True, "symlinks": True, + "timestamps_ignore_archive_entry_mtimes": False, + # The tree has no .picopt.yaml files: the empty-digest fingerprint. + "_dir_config_fingerprint": sha256(b"").hexdigest(), } TREESTAMPS_CONFIG = {"ignore": frozenset(), "symlinks": True} diff --git a/tests/test_walk_skip.py b/tests/test_walk_skip.py new file mode 100644 index 0000000..051cd38 --- /dev/null +++ b/tests/test_walk_skip.py @@ -0,0 +1,153 @@ +"""Test walk skip rules and per-member error resilience.""" + +import os +from pathlib import Path +from typing import Any + +import pytest +from treestamps import Treestamps + +from picopt import PROGRAM_NAME, cli +from picopt.config import PicoptConfig +from picopt.config.settings import PicoptSettings +from picopt.log.reporter import Reporter +from picopt.path import PathInfo +from picopt.walk.scheduler import ContainerNode +from picopt.walk.skip import WalkSkipper +from picopt.walk.walk import Walk + +__all__ = () + +_DATA = b"x" * 32 + + +def _make_config(*args: str) -> PicoptSettings: + return PicoptConfig().get_config(cli.get_arguments(("picopt", *args, "."))) + + +def _make_skipper(*args: str) -> WalkSkipper: + return WalkSkipper(_make_config(*args), Reporter()) + + +class TestWalkSkipper: + """Skip rule unit tests.""" + + def test_timestamps_file_skipped_by_basename(self: Any, tmp_path: Path) -> None: + """Treestamps yaml files are skipped even when named by full path.""" + skipper = _make_skipper() + fname = next(iter(Treestamps.get_filenames(PROGRAM_NAME))) + stamp_path = tmp_path / fname + stamp_path.write_bytes(_DATA) + path_info = PathInfo(top_path=tmp_path, path=stamp_path, convert=False) + assert skipper.is_walk_file_skip(path_info) + + def test_dir_config_file_skipped_by_basename(self: Any, tmp_path: Path) -> None: + """.picopt.yaml files are never optimization targets.""" + skipper = _make_skipper() + config_path = tmp_path / ".picopt.yaml" + config_path.write_bytes(_DATA) + path_info = PathInfo(top_path=tmp_path, path=config_path, convert=False) + assert skipper.is_walk_file_skip(path_info) + + def test_ignore_respects_case_sensitivity(self: Any, tmp_path: Path) -> None: + """Uppercase ignore patterns don't match lowercase files on case-sensitive filesystems.""" + skipper = _make_skipper("-i", "*.BAK") + path = tmp_path / "keep.bak" + path.write_bytes(_DATA) + sensitive = PathInfo( + top_path=tmp_path, path=path, convert=False, is_case_sensitive=True + ) + assert not skipper.is_walk_file_skip(sensitive) + insensitive = PathInfo( + top_path=tmp_path, path=path, convert=False, is_case_sensitive=False + ) + assert skipper.is_walk_file_skip(insensitive) + + @pytest.mark.skipif(not hasattr(os, "mkfifo"), reason="platform lacks mkfifo") + def test_fifo_skipped_without_blocking(self: Any, tmp_path: Path) -> None: + """Named pipes are skipped instead of blocking the walk forever.""" + skipper = _make_skipper() + fifo_path = tmp_path / "pipe.png" + os.mkfifo(fifo_path) + path_info = PathInfo(top_path=tmp_path, path=fifo_path, convert=False) + assert skipper.is_walk_file_skip(path_info) + + +class _FakeNodeHandler: + """Container handler stand-in collecting noop members.""" + + def __init__(self) -> None: + self._optimized_contents: set[Any] = set() + + def get_optimized_contents(self) -> set[Any]: + return self._optimized_contents + + +class TestEnqueueChildrenResilience: + """A member that explodes during detection is one error, not a run-ender.""" + + def test_detection_error_keeps_member_and_continues( + self: Any, monkeypatch: pytest.MonkeyPatch + ) -> None: + """The failed member is passed through; later members still process.""" + walk = Walk(_make_config()) + try: + + def _bomb(_path_info: PathInfo) -> None: + msg = "simulated DecompressionBombError" + raise ValueError(msg) + + monkeypatch.setattr(walk, "_create_handler", _bomb) + handler = _FakeNodeHandler() + node = ContainerNode(handler=handler) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + children = [ + PathInfo( + top_path=Path(), + path=None, + convert=False, + is_case_sensitive=True, + data=_DATA, + ) + for _ in range(2) + ] + walk._enqueue_children(None, node, children) # pyright: ignore[reportArgumentType] # ty: ignore[invalid-argument-type] + assert handler.get_optimized_contents() == set(children) + finally: + walk._executor.shutdown(wait=False) + + +class TestWalkResilience: + """Hostile directory trees must not crash or hang the walk.""" + + def test_symlink_directory_loop_terminates_cleanly( + self: Any, tmp_path: Path + ) -> None: + """A symlink cycle is walked once, with no errors.""" + from picopt import PROGRAM_NAME, cli + from tests import IMAGES_DIR + + subdir = tmp_path / "sub" + subdir.mkdir() + (subdir / "loop").symlink_to(tmp_path, target_is_directory=True) + png = subdir / "test_png.png" + png.write_bytes((IMAGES_DIR / "test_png.png").read_bytes()) + orig_size = png.stat().st_size + + cli.main((PROGRAM_NAME, "-rvx", "PNG", str(tmp_path))) + + assert png.stat().st_size < orig_size + + def test_unreadable_dir_does_not_crash_legacy_import( + self: Any, tmp_path: Path + ) -> None: + """Legacy timestamp import skips unreadable directories.""" + from picopt import PROGRAM_NAME, cli + + (tmp_path / ".picopt_timestamp").touch() + locked = tmp_path / "locked" + locked.mkdir() + locked.chmod(0o000) + try: + cli.main((PROGRAM_NAME, "-rtv", str(tmp_path))) + finally: + locked.chmod(0o755) diff --git a/tests/test_webp_lossless.py b/tests/test_webp_lossless.py new file mode 100644 index 0000000..cd990db --- /dev/null +++ b/tests/test_webp_lossless.py @@ -0,0 +1,79 @@ +"""Test webp lossless detection via RIFF chunk parsing.""" + +from io import BytesIO +from struct import pack + +from picopt.pillow.png_bit_depth import png_bit_depth +from picopt.pillow.webp_lossless import is_lossless +from tests import IMAGES_DIR + +__all__ = () + +LOSSLESS_FN = "test_webp_lossless.webp" +LOSSY_FN = "test_webp_lossy.webp" +ANIMATED_FN = "test_animated_webp.webp" + + +def _chunk(fourcc: bytes, payload: bytes) -> bytes: + data = fourcc + pack(" bytes: + body = b"WEBP" + b"".join(chunks) + return b"RIFF" + pack(" None: + for fn, expected in ( + (LOSSLESS_FN, True), + (LOSSY_FN, False), + (ANIMATED_FN, True), + ): + path = IMAGES_DIR / fn + with path.open("rb") as f: + assert is_lossless(f) == expected, fn + assert is_lossless(BytesIO(path.read_bytes())) == expected, fn + + def test_lossless_with_large_icc_profile_in_buffer(self) -> None: + """A big metadata chunk before VP8L must not defeat detection.""" + data = _riff( + _chunk(b"VP8X", b"\x00" * 10), + _chunk(b"ICCP", b"\x00" * 4096), + _chunk(b"VP8L", b"\x2f" * 32), + ) + assert is_lossless(BytesIO(data)) is True + + def test_lossy_with_metadata_containing_vp8l_bytes(self) -> None: + """'VP8L' inside a metadata payload must not fake losslessness.""" + data = _riff( + _chunk(b"VP8X", b"\x00" * 10), + _chunk(b"EXIF", b"decoy VP8L decoy"), + _chunk(b"VP8 ", b"\x00" * 32), + ) + assert is_lossless(BytesIO(data)) is False + + def test_buffer_left_open_and_rewound(self) -> None: + """The caller's buffer survives detection.""" + buffer = BytesIO((IMAGES_DIR / LOSSLESS_FN).read_bytes()) + is_lossless(buffer) + assert not buffer.closed + assert buffer.tell() == 0 + + def test_empty_and_truncated_input(self) -> None: + assert is_lossless(BytesIO(b"")) is False + assert is_lossless(BytesIO(b"RIFF\x00\x00\x00\x00WEBP")) is False + assert is_lossless(BytesIO(b"not a webp at all")) is False + + +class TestPngBitDepth: + """Truncated PNGs must not report a fake bit depth.""" + + def test_truncated_png_returns_none(self) -> None: + header_only = (IMAGES_DIR / "test_png.png").read_bytes()[:16] + assert png_bit_depth(BytesIO(header_only)) is None diff --git a/tests/test_write_config.py b/tests/test_write_config.py new file mode 100644 index 0000000..ebdab4b --- /dev/null +++ b/tests/test_write_config.py @@ -0,0 +1,138 @@ +"""Test the -w / -W / --write-config-file config writers.""" + +import os +import stat +import sys +from pathlib import Path + +import pytest +from confuse.exceptions import ConfigError +from ruamel.yaml import YAML + +from picopt import PROGRAM_NAME, cli +from picopt.config import PicoptConfig, _target_dir_config_paths +from picopt.config.settings import PicoptSettings + +__all__ = () + +OWNER_ONLY_MODE = 0o600 + + +@pytest.fixture(autouse=True) +def _isolate_config(monkeypatch, tmp_path): + """Point confuse at an empty config dir and scrub picopt env vars.""" + for key in list(os.environ): + if key.startswith("PICOPT"): + monkeypatch.delenv(key, raising=False) + monkeypatch.setenv("PICOPTDIR", str(tmp_path)) + + +def _get_config(argv: tuple[str, ...]) -> PicoptSettings: + return PicoptConfig().get_config(cli.get_arguments(argv)) + + +def _load_section(path: Path) -> dict: + data = YAML().load(path.read_text()) + return dict(data[PROGRAM_NAME]) + + +class TestWriteConfig: + """-w persists invoked options to the user config.""" + + def test_written_config_round_trips(self, tmp_path: Path) -> None: + _get_config(("picopt", "-w", "-rb", "-x", "CBZ", str(tmp_path))) + config_path = tmp_path / "config.yaml" + assert config_path.is_file() + # The user config dir is PICOPTDIR, so a plain invocation reads it. + config = _get_config(("picopt", str(tmp_path))) + assert config.recurse is True + assert config.bigger is True + assert "CBZ" in config.formats + # And explicitly via -C as well. + config = _get_config(("picopt", "-C", str(config_path), str(tmp_path))) + assert config.recurse is True + + def test_run_mode_flags_and_paths_not_persisted(self, tmp_path: Path) -> None: + _get_config(("picopt", "-w", "-rdLq", str(tmp_path))) + section = _load_section(tmp_path / "config.yaml") + assert "recurse" in section + for absent in ( + "dry_run", + "list_only", + "verbose", + "paths", + "config", + "write_config", + "write_dir_config", + "write_config_file", + ): + assert absent not in section + + def test_tuple_options_written_as_lists(self, tmp_path: Path) -> None: + _get_config(("picopt", "-w", "-x", "CBZ,ZIP", str(tmp_path))) + section = _load_section(tmp_path / "config.yaml") + assert sorted(section["extra_formats"]) == ["CBZ", "ZIP"] + + def test_comments_and_unrelated_keys_survive(self, tmp_path: Path) -> None: + config_path = tmp_path / "config.yaml" + config_path.write_text("# keep this comment\npicopt:\n near_lossless: true\n") + _get_config(("picopt", "-w", "-r", str(tmp_path))) + text = config_path.read_text() + assert "# keep this comment" in text + section = _load_section(config_path) + assert section["near_lossless"] is True + assert section["recurse"] is True + + @pytest.mark.skipif(sys.platform == "win32", reason="POSIX modes only") + def test_written_config_owner_only(self, tmp_path: Path) -> None: + _get_config(("picopt", "-w", "-r", str(tmp_path))) + mode = stat.S_IMODE((tmp_path / "config.yaml").stat().st_mode) + assert mode == OWNER_ONLY_MODE + + def test_invalid_invocation_does_not_write(self, tmp_path: Path) -> None: + with pytest.raises(ConfigError): + _get_config(("picopt", "-w", "--memory-limit", "banana", str(tmp_path))) + assert not (tmp_path / "config.yaml").exists() + + def test_write_config_file_explicit_path(self, tmp_path: Path) -> None: + target = tmp_path / "out" / "myconfig.yaml" + _get_config(("picopt", "--write-config-file", str(target), "-r", str(tmp_path))) + assert target.is_file() + assert _load_section(target)["recurse"] is True + + +class TestWriteDirConfig: + """-W writes .picopt.yaml into each target directory.""" + + def test_write_dir_config_each_target_dir(self, tmp_path: Path) -> None: + dir_a = tmp_path / "a" + dir_b = tmp_path / "b" + dir_a.mkdir() + dir_b.mkdir() + _get_config(("picopt", "-W", "-b", str(dir_a), str(dir_b))) + for directory in (dir_a, dir_b): + section = _load_section(directory / ".picopt.yaml") + assert section["bigger"] is True + + def test_file_target_writes_parent(self, tmp_path: Path) -> None: + target_file = tmp_path / "img.png" + target_file.write_bytes(b"x") + _get_config(("picopt", "-W", "-b", str(target_file))) + assert (tmp_path / ".picopt.yaml").is_file() + + def test_updates_existing_dir_config_in_place(self, tmp_path: Path) -> None: + dir_config = tmp_path / ".picopt.yaml" + dir_config.write_text("picopt:\n keep_metadata: false\n") + _get_config(("picopt", "-W", "-b", str(tmp_path))) + section = _load_section(dir_config) + assert section["keep_metadata"] is False + assert section["bigger"] is True + + +def test_target_dir_config_paths_dedupes_and_uses_parent(tmp_path: Path) -> None: + """File targets map to their parent; duplicates collapse.""" + target_file = tmp_path / "img.png" + target_file.write_bytes(b"x") + paths = (str(tmp_path), str(target_file), str(tmp_path)) + targets = _target_dir_config_paths(paths) + assert targets == [tmp_path / ".picopt.yaml"] diff --git a/uv.lock b/uv.lock index 945cc86..d172cdb 100644 --- a/uv.lock +++ b/uv.lock @@ -78,95 +78,95 @@ wheels = [ [[package]] name = "backports-zstd" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/05/480d439b482edf59b786bc19b474d990c61942e372f5de3dc14acac8154d/backports_zstd-1.5.0.tar.gz", hash = "sha256:a5e622a82eb183b4fbe18032755ce0a15fa9a82f2adb9b621620b91247aaedb7", size = 998556, upload-time = "2026-05-11T19:54:24.923Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/6e/bc24b45e16381272db45bfe627c1762600fc5fbcd39cef3723c89425129e/backports_zstd-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09045a00d9dad12dab49e029b26c197637b882cf4adc737a373404ba2aaabbca", size = 436832, upload-time = "2026-05-11T19:51:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e2/87/85bc9b98bd0bbbe76af0aa19d423eb93906467110e4cdd4741fd8d26def5/backports_zstd-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e51edd66db6855bee020c951ca5c2e816777bfe77f87742fbbfae9a32d482fec", size = 363217, upload-time = "2026-05-11T19:51:56.359Z" }, - { url = "https://files.pythonhosted.org/packages/c1/61/b461cf3620ee3a55e20d885ef61c5ab56a3745ccc0d422f74968337777ca/backports_zstd-1.5.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:73ff4ceb7e28538455e0a44f53e05a731bbdb9bfe2cab4a1637dd1f0093732e3", size = 507163, upload-time = "2026-05-11T19:51:57.957Z" }, - { url = "https://files.pythonhosted.org/packages/ed/cb/4e0063bf90d6fd17329ff271e131758d5d96a73061b6d45577a8be6ebf42/backports_zstd-1.5.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9526d69c8fbef03e04d74b33946e23f806399cb49e51550bb21d757fb2ce869", size = 476728, upload-time = "2026-05-11T19:51:59.822Z" }, - { url = "https://files.pythonhosted.org/packages/11/4a/ee0c81e24789781fcc8399817e5c82121001293dbbaf17629833ff0d34e8/backports_zstd-1.5.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e24ee1e1bbb4549a2ad63695b4a5776596aa171fdaf7c1e178e61e351faf0a9", size = 582391, upload-time = "2026-05-11T19:52:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/e3/aa/3c2c28492656af005ed9602beab4c20813346b53257413ae57bf88adbd41/backports_zstd-1.5.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ebfbf7307d618d68deef905d3d6655339d4ce187e176023bff8fbd44ec1e20d0", size = 642040, upload-time = "2026-05-11T19:52:03.396Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ad/9070e691597657bd3b983d8c8ba46bc6ee4d394608e7be969f2060f16899/backports_zstd-1.5.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b82506a4da0977754335c727752411bbba1fe476a8662d96161218f275fba859", size = 492266, upload-time = "2026-05-11T19:52:05.16Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ec/7222e9e8ca899cf9d538468b0fb6386da93dae94f6e60625a7ef99281672/backports_zstd-1.5.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cf8355cdfa7a2cba9c51655d56e6be39c751799286b142640be30fef2301a70", size = 566215, upload-time = "2026-05-11T19:52:07.037Z" }, - { url = "https://files.pythonhosted.org/packages/9f/f8/bf880d87cfb71ad9753142d2ad0802015ee4a343b8c080ea6f0eb6b05bfb/backports_zstd-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f7de15f3871d21d6e761c5a309618b069fee5f225e64e4406956ac0209dc6917", size = 482662, upload-time = "2026-05-11T19:52:08.726Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ed/fc7144651682744b32de1e624bcad6d0bb72d6359e37a5d9e980f3d5a45b/backports_zstd-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:624825b9c290e6089cd9955d88da04b085528fe213adf3e4e8be5c0fffef6c65", size = 510592, upload-time = "2026-05-11T19:52:10.244Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/436ee1aa915fa310d0e83c361f25757960f96ef798f532948351637125fd/backports_zstd-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7088a75f96d8f6b0d3523ec3a99d1472ce03c3524b2f7b485b80e115ef20055f", size = 586713, upload-time = "2026-05-11T19:52:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/55/9b/16573be05e8fe54cb356d9aa9aeb84d1e14fd49fe23ea7f261027e2e7f25/backports_zstd-1.5.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:97f4d29e99538b11313cbc7a6d9b3c2ce0d69fdc497699ab74953d0d5949ab88", size = 564032, upload-time = "2026-05-11T19:52:13.864Z" }, - { url = "https://files.pythonhosted.org/packages/95/33/7cf01fb8b4cff1ea6c7fee19d64de8a1a8dec7b18703af2aca79c8f87864/backports_zstd-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8b4e17632759a45a7d0c4cf31968d8d033eefbe1a3d81d8aaf519558371c3359", size = 632604, upload-time = "2026-05-11T19:52:15.469Z" }, - { url = "https://files.pythonhosted.org/packages/1a/03/547b4e0abf8e1c2f29314e1e3ed7a3e2054b22560b2bad843423fbb99140/backports_zstd-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0075195c79c0508bc7313a3402b187bd9d27d4f9a376e8e2caac0fc2baeacbdf", size = 496272, upload-time = "2026-05-11T19:52:17.064Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ff/28c94189774b62c26ddf65ee54ec3591f6f0217d9545d20854f8600541b0/backports_zstd-1.5.0-cp310-cp310-win32.whl", hash = "sha256:11c694c9eef69c19a52df94466d4fd5c8b1bdfbaad350e95adc883b40d8b3be2", size = 289665, upload-time = "2026-05-11T19:52:18.946Z" }, - { url = "https://files.pythonhosted.org/packages/18/0e/579f193d023c099ecaf560aae72701bfa6bacc5486cf57f91236b9c1404e/backports_zstd-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1ea900765329a515020e4e66c65a826657cc1f110770cac3f71ec01b43f2d25", size = 314698, upload-time = "2026-05-11T19:52:20.734Z" }, - { url = "https://files.pythonhosted.org/packages/7e/f7/1cfc87f0171268ffb3eb479f0b8ef936164cbb6bddd1fbf1457e1ac8aecb/backports_zstd-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:0c473387025e233d123f401d09a17a57e0b9af2ec2423aae7f50f1c806887cb3", size = 291362, upload-time = "2026-05-11T19:52:22.486Z" }, - { url = "https://files.pythonhosted.org/packages/26/bc/083c0ebee316f4863ed288c4a5eaa1e98be115e82deb8855da8bab1c7701/backports_zstd-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fbaa5502617dc4f04327c7a2951f0fcdca7aaef93ddf32c15dc8b620208174fa", size = 436838, upload-time = "2026-05-11T19:52:24.349Z" }, - { url = "https://files.pythonhosted.org/packages/cd/e5/bf778667fff6598dbd0791745123ed964aee94753ae8e4e92aa1e07417b6/backports_zstd-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:204f00d62e95aab987c7c019452b2373bdefb17252443765f2ede7f15b6e669a", size = 363215, upload-time = "2026-05-11T19:52:25.887Z" }, - { url = "https://files.pythonhosted.org/packages/63/a5/4fae78734dbefcb4b5386137c807e2107c4bc94e85c0d9eaae79206dde84/backports_zstd-1.5.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:2c77c0d4c330afd26d2a98f3d689ab922ec3f046014a1614ddcaad437666ac05", size = 507161, upload-time = "2026-05-11T19:52:27.48Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ec/b64409f0cf56fb65181d6f5d9130058f19d5c3c9f8c581a5e2bd62642630/backports_zstd-1.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bb2f2d2c07358edeaa251cf804b993e9f0d5d93af8a7ea2414d80ff3c105e95", size = 476728, upload-time = "2026-05-11T19:52:29.182Z" }, - { url = "https://files.pythonhosted.org/packages/4d/10/4c1693cb4e129585a6e4cb565106cad7347e61c43c8375b9e9cadb00eb06/backports_zstd-1.5.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89f554abcebcb2c487024e63be8059083775c5fd351fec0cc2dc3e9f528714", size = 582388, upload-time = "2026-05-11T19:52:30.908Z" }, - { url = "https://files.pythonhosted.org/packages/45/b9/dc748a0e7d21ce2228241f6e8af96d297c80ab69c4c49429309b8fa3beb8/backports_zstd-1.5.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea969758af743000d822fc3a69dc9de059bbbb8d07d2f13e06ff49ac63cce74f", size = 642091, upload-time = "2026-05-11T19:52:32.397Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/02366ddae6e008d53df71605e4e3ca8dcea5d1dfcba29040b46883a23127/backports_zstd-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:775ad82d268923639bc924013fc61561df376c148506b241f0f80718b5bb3a2f", size = 492256, upload-time = "2026-05-11T19:52:34.441Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/c5e7824c17abc87dbb24c7c90dc43054d701533cf04d3531cb9b7105cdac/backports_zstd-1.5.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:663128370bbc2ebcc436b8977bc434a7bf29919d92d91fee05ed6fb0fa807646", size = 566214, upload-time = "2026-05-11T19:52:35.962Z" }, - { url = "https://files.pythonhosted.org/packages/12/7b/ee7368c4ad8f5e00b3fd84fc566fb7714aa766c5672500793990e19efa00/backports_zstd-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:572c76832e9a24da4084befa52c23f4c03fede2aa250ae6250cbc5a11b980f69", size = 482666, upload-time = "2026-05-11T19:52:37.675Z" }, - { url = "https://files.pythonhosted.org/packages/77/36/2826f9f04b6c91d5f707f49188ac6f5ec7487b36d73caedfa20db3307826/backports_zstd-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9410bcbcd3afd787a15a276d68f954d1703788c780faa421183a61d39da8b862", size = 510594, upload-time = "2026-05-11T19:52:39.501Z" }, - { url = "https://files.pythonhosted.org/packages/84/3b/95342baf0e301b7d06c6862389f8520a9d71f073a6c1a5b86182e7d89148/backports_zstd-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0fab15e6895bef621041dd82d6306ffa24889257dd902c4b98b88e4260b3465d", size = 586713, upload-time = "2026-05-11T19:52:41.461Z" }, - { url = "https://files.pythonhosted.org/packages/bc/32/73d2b8f572960307406b084bb8932f4ebd9fcedb05d1502e04fecf25970a/backports_zstd-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ffde637b6d0082f1c3356657002469cf199c7c12d50d9822a55b13425c778d3", size = 564037, upload-time = "2026-05-11T19:52:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a4/6e319fa7fa5851c3ca9701cbded9522c16018432a01a33a95cc0fccb6b4a/backports_zstd-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c01d377c1489cb2230bf6a9ff01c73c42863cc96ee648c49923d4f6d4ea4e2d5", size = 632626, upload-time = "2026-05-11T19:52:45.017Z" }, - { url = "https://files.pythonhosted.org/packages/67/5c/10df0444db05f9276b286d230a3d6948ad47c593fc22925b8fe551d34b26/backports_zstd-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4080bb9c8a51bb2bf8caf8018d78278cd49eb924cb06a54f56a411095e2ac912", size = 496270, upload-time = "2026-05-11T19:52:46.558Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ad/6cd1de5cd858ac653833098f13a4643a4c9db484072350d3dbf299cc46f1/backports_zstd-1.5.0-cp311-cp311-win32.whl", hash = "sha256:9f4fe3fd82c8c6e8a9fdc5c71f92f9fe2442d02e7f59fddef25a955e189e3f38", size = 289754, upload-time = "2026-05-11T19:52:48.232Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1b/df94ad1cb79705d717f7e1063da642c538a6d7ce6443c8e60355fa507ea4/backports_zstd-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:e7c0372fa036751109604c70a8c87e59faaacc195d519c8cb9e0e527ee2b5478", size = 314829, upload-time = "2026-05-11T19:52:50.031Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/24e60da7cc89b9ed1c5b474678e316dd0ddfe7cd1de39b23d04452ca5946/backports_zstd-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:264a66137555bb4648f7e64cfc514d820758072684f373269fcdd2e8d4a90306", size = 291497, upload-time = "2026-05-11T19:52:51.729Z" }, - { url = "https://files.pythonhosted.org/packages/24/71/29ed213344f8f62b7520745d7df3752d88db456aff9d8b706bdf5eb99a3c/backports_zstd-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1858cacdb3e50105a1b60acdc3dd5b18650077d12dce243e19d5c88e8172bd71", size = 437170, upload-time = "2026-05-11T19:52:53.204Z" }, - { url = "https://files.pythonhosted.org/packages/d0/e3/a58a3eb8fc54d4e3e4f684ed7b1f688da02e5bda5ae5e2809e94cf2ead2f/backports_zstd-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ccffc0a1974ecc2cc42afa4c15f56d036a4b2bae0abc46e6ba9b3358d9b1c037", size = 363265, upload-time = "2026-05-11T19:52:55.153Z" }, - { url = "https://files.pythonhosted.org/packages/3f/03/9d13840d206dec1c4698c803f61c58379b3578cb9dc6140ba5fa4ce2f31d/backports_zstd-1.5.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:ab3430ab4d4ac3fb1bc1e4174d137731e51363b6abd5e51a1599690fe9c7d61d", size = 507527, upload-time = "2026-05-11T19:52:57.256Z" }, - { url = "https://files.pythonhosted.org/packages/6a/8f/8dc4b5736dca218cbca9609549a8f6dc202990abdb49afdc6112442f5360/backports_zstd-1.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c737c1cb4a10c2d0f6cba9a347522858094f0a737b4558c67a777bcaa4a795cd", size = 477352, upload-time = "2026-05-11T19:52:59.425Z" }, - { url = "https://files.pythonhosted.org/packages/96/2c/65a66976a761b5b62eacbaed5ed418c694b24b5c480399315d799751de62/backports_zstd-1.5.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0379c66510681a6b2780d3f3ef2cff54d01204b52448d64bde1855d40f856a04", size = 582799, upload-time = "2026-05-11T19:53:01.303Z" }, - { url = "https://files.pythonhosted.org/packages/d3/e9/ee93a66cd28cb3ad7f3c04d1105325a5428671b18bd41ba9ed8b43bc44cf/backports_zstd-1.5.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c7474b291e264c9609358d3875cf539623f7a65339c2b533020992b1a4c095b", size = 641530, upload-time = "2026-05-11T19:53:03.082Z" }, - { url = "https://files.pythonhosted.org/packages/e4/4b/2cecd4d6679f175f28ae02022bd2050ff4023e38902fae104dbe2e231911/backports_zstd-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb73c22444617bc5a3abf32dd27b3f2085898cfe3b95e6855300e9189898a3bd", size = 495324, upload-time = "2026-05-11T19:53:05.005Z" }, - { url = "https://files.pythonhosted.org/packages/4d/20/ee21e4e791e31f38f7a70b3961eb64b350d9be802a335e7a04c02b41b197/backports_zstd-1.5.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6cd7f6c33afd89354f74469e315e72754e3040f91f7b685061e225d9e36e3e8e", size = 569796, upload-time = "2026-05-11T19:53:07.011Z" }, - { url = "https://files.pythonhosted.org/packages/76/da/86c9a2ea384885b60638b3e47113198449568d0e36ef3834d1f969623092/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2106309071f279b38d3663c55c7fed192733b4f332b50eb3fa707e54bad6967a", size = 483367, upload-time = "2026-05-11T19:53:08.674Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f0/c95c6e4dd28fc314547782a482839e422283d62c2aaf45d30672109a4a1e/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:56fffa80be74cb11ac843333bbdc56e466c87967706886b3efd6b16d83830d90", size = 510976, upload-time = "2026-05-11T19:53:10.339Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a2/72777b7e1872228a13b09b0bf77ae6cf626008d462cc2e1a0ae64721fd55/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5e8b8251eec80e67e30ec79dfc5b3b1ada069b9ac48b56b102f3e2c6f8281062", size = 587190, upload-time = "2026-05-11T19:53:12.205Z" }, - { url = "https://files.pythonhosted.org/packages/f5/a1/db5d1aee59da308eadeaa189764a4ec68e98495c309a13dcb8da5718fef1/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f334dd17ffead361aa9090e40151bd123507ce213a62733121b7145c6711cbde", size = 567395, upload-time = "2026-05-11T19:53:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/00/0f/39ca1a6e8c5c2dc81da9e06c44d1990cc464f4b16dae214e877afd7adfc0/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:78cbfd061255fef6de5070a54e0f9c00e8aabad5c99dd2ad884a3a7d1acc09ae", size = 632048, upload-time = "2026-05-11T19:53:16.234Z" }, - { url = "https://files.pythonhosted.org/packages/73/fd/a438ee4fc615016dbe96112b709b6805ee19eb215f46e208c8fbce086d8d/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f55d70df44f49d599e20033013bc1ae705202735c45d4bca8eb963b225e15fd", size = 499833, upload-time = "2026-05-11T19:53:17.85Z" }, - { url = "https://files.pythonhosted.org/packages/f7/42/f544fde4de32687e28c514288ae3c11106ba644e9dd580992cbd704bbb49/backports_zstd-1.5.0-cp312-cp312-win32.whl", hash = "sha256:a8b096e0383a3bcab34f8c97b79e1a52051189d11258bbc2bc1145997a15dd1d", size = 289876, upload-time = "2026-05-11T19:53:19.486Z" }, - { url = "https://files.pythonhosted.org/packages/ad/31/9c29cd3175892e5ee909f5e8d14707fa07815301ff24b5c697d1cea62a77/backports_zstd-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:e2802899ba4ef1a062ffe4bb1292c5df32011a54b4c3004c54f46ec975f39554", size = 314933, upload-time = "2026-05-11T19:53:20.942Z" }, - { url = "https://files.pythonhosted.org/packages/11/ee/1a50acd6446c0d57c4f93ad6ce68e1a631ad920737a6b2d0bbbc47de7f42/backports_zstd-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:3c0353e66942afbd45518788cfbd1e9e117828ceb390fa50517f46f291850d8e", size = 291665, upload-time = "2026-05-11T19:53:22.686Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e6/252521e3a847eb200bc0a1d528542d651b9c8dc7953e231c39ed2890d5ff/backports_zstd-1.5.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:02a57ee8598dd863c0b11c7af00042ce6bc045bf6f4249fa4c322c62614ca1fd", size = 400134, upload-time = "2026-05-11T19:53:24.28Z" }, - { url = "https://files.pythonhosted.org/packages/36/43/27ef105ffa2da3d52218d4a7b2e14037974283953b3ee790358af6e9b4df/backports_zstd-1.5.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c56c11eb3173d540e1fb0216f7ab477cbd3a204eca41f5f329059ee8a5d2ad47", size = 454225, upload-time = "2026-05-11T19:53:25.874Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c9/cdcba1244347500d00567ce2cd6bf04c92d1b0fb6405fb8e13c07715eb46/backports_zstd-1.5.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ef98f632026aa8e6ce05d786977092798efbe78677aa71219f22d31787809c90", size = 357229, upload-time = "2026-05-11T19:53:27.661Z" }, - { url = "https://files.pythonhosted.org/packages/df/da/cea04dab3ffb940bde9a59866bde6f2594a7b3ef2948a63fb3898f73d311/backports_zstd-1.5.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:c3712300b18f9d07f788b03594b2f34dfad89d77df96938a640c5007522a6b69", size = 365907, upload-time = "2026-05-11T19:53:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/da/c4/6a71df2e65033f9b7d8017d77ea2bb572fc2ebc814ea383fdcda4187597a/backports_zstd-1.5.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:bdbc75d1f54df70b65bcfbc8aa0cac21475f79665bb045960af606dc07b56090", size = 446453, upload-time = "2026-05-11T19:53:30.888Z" }, - { url = "https://files.pythonhosted.org/packages/66/e7/f98ad1a6a249c27884df9d28cf6ebc3c368e0e3288a741c1d51a572bb3d7/backports_zstd-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93d306300d25e59f1cbe98cda494bf295be03a20e8b2c5602ee5ddc03ded29f2", size = 436634, upload-time = "2026-05-11T19:53:32.484Z" }, - { url = "https://files.pythonhosted.org/packages/ba/42/d0393ecc64e2ab6ae1b5ca7edbe26e3fe5196885f15d6cc4bce7254e29cd/backports_zstd-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:305d2e4ae9a595d0fd9d5bea5a7a2163306c6c4dcc5eec35ecd5008219d4580e", size = 362867, upload-time = "2026-05-11T19:53:34.385Z" }, - { url = "https://files.pythonhosted.org/packages/41/fe/87aa9404763bada695d06e5cb9d0575bae033cbf3a2e4e3bd648760178f7/backports_zstd-1.5.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:c8f0967bf8d806b250fb1e905a6b8190e7ae83656d5308989243f84e01fa3774", size = 506844, upload-time = "2026-05-11T19:53:36.023Z" }, - { url = "https://files.pythonhosted.org/packages/56/94/3af7ce637d148e0b0acb1298b61afe9a934ed425bad9ff05e87afbf6766d/backports_zstd-1.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76b7314ca9a253171e3e9524960e9e6411997323cf10aecbbc330faa7a90278d", size = 476975, upload-time = "2026-05-11T19:53:37.885Z" }, - { url = "https://files.pythonhosted.org/packages/aa/6c/dc2aa1b48296ac6effc3bacb5a3061d40ed74bf73082dfe38eed2ba8362b/backports_zstd-1.5.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1d0bf16bba86b1071731ced389f184e8de61c1afcafa584244f7f726632f92f", size = 582496, upload-time = "2026-05-11T19:53:39.812Z" }, - { url = "https://files.pythonhosted.org/packages/f6/38/dd49d3dd27eda9b165ccd63d70538fea016a3e9e42923bbbc1d89fae8a43/backports_zstd-1.5.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:96709d27d406008575ef759405169d538040156704b457d8c0ac035127a46b67", size = 643257, upload-time = "2026-05-11T19:53:41.819Z" }, - { url = "https://files.pythonhosted.org/packages/59/75/78e819272450aec2462f97a1bceb90bde481f9dba435bf9e76d580b4dec4/backports_zstd-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5737402c29b2bd5bc661d4cde08aed531ed326f2b59a7ad98dc07650dc99a2c9", size = 491958, upload-time = "2026-05-11T19:53:43.501Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/d860f9cf21cb59d583a12166353bf71a439538e2b669f4a7736e400ca596/backports_zstd-1.5.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b65f37ddd375114dbf84658e7dd168e10f5a93394940bfefa7fafc2d3234450", size = 567198, upload-time = "2026-05-11T19:53:45.226Z" }, - { url = "https://files.pythonhosted.org/packages/38/7c/b175d4c9ff60f964c8f6dd43211de905227cfde5a41eb5f654df58483025/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fae7825dde4f81c28b4c66b1e997f893e296c3f1668351952b3ed085eb9f8cd", size = 482792, upload-time = "2026-05-11T19:53:47.323Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e3/f7b50cf891a10da5f9c412ed4a9c4a772df4d4186d98a41e75c9b462f148/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3aa10e77c0e712d2dfb950910b50591c2fb11f0f1328814e23acc0b4950766df", size = 510363, upload-time = "2026-05-11T19:53:49.523Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/e7841fd4a65661d527697a0e2dab97295868965ccd4e3e12474472719a60/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:518b2ef54ce0fee6d29379cfd64ef66e639456f1b18943466e929b19677f135f", size = 586917, upload-time = "2026-05-11T19:53:51.741Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7c/57e985dbd621f0307b8c57cabb258eb976793f2aeaf8a5bc020e15b4a793/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:673a1e5fdaa6cb0c7a967eb33066b6dd564871b3498a93e11e2972998047d11f", size = 565004, upload-time = "2026-05-11T19:53:53.774Z" }, - { url = "https://files.pythonhosted.org/packages/2f/8f/855ffcd1ee0fcf44c3fe62e36db8e7362292d450cc7c4b3f43edccbcd37a/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1277c07ff2d731586aa05aebd946a1b30184620d886a735dd5d5bf94a4a1061e", size = 633737, upload-time = "2026-05-11T19:53:56.036Z" }, - { url = "https://files.pythonhosted.org/packages/20/39/c4129a03d268699200dfebe1ccab97c7c332d2794571afb372a62e4ed098/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aff334c7c38b4aea2a899f3138a99c1d58f0686ad7815c74bff506ecf4333296", size = 496309, upload-time = "2026-05-11T19:53:57.591Z" }, - { url = "https://files.pythonhosted.org/packages/8e/33/34152316dd244dcd43d5300ded3cf6e1b46d343e4e92620c23e533fa91df/backports_zstd-1.5.0-cp313-cp313-win32.whl", hash = "sha256:b932834c4d85360f46d1e7fbf3eee1e26ba594e0eb5c3ee1281e89bc1d48d06f", size = 289560, upload-time = "2026-05-11T19:53:59.274Z" }, - { url = "https://files.pythonhosted.org/packages/71/c5/f759bc87fd77c88f4fdad2d878535fb7e9537c6a05876d206e6690bf33c6/backports_zstd-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:c71dfbeced720326a8917a6edf921c568dc2396228c6432205c6d7e7fe7f3707", size = 314812, upload-time = "2026-05-11T19:54:00.909Z" }, - { url = "https://files.pythonhosted.org/packages/47/96/d7970dbb2fef34b549b34146090f48f41903cc7268b1ed1c7542eaa1852e/backports_zstd-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:7b5798b20ffff71ee4620a01f56fe0b50271724b4251db08c90a069446cc4752", size = 291411, upload-time = "2026-05-11T19:54:02.541Z" }, - { url = "https://files.pythonhosted.org/packages/5b/35/294ce0d818455191ee9a0f21d987d6061d4f844ca34ca44a8b1daaaba3ca/backports_zstd-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9685586eb67fa2e59eab8027d48e8275ce90e404b6dc737b508f741853ba6cb7", size = 410912, upload-time = "2026-05-11T19:54:04.031Z" }, - { url = "https://files.pythonhosted.org/packages/1a/5c/99fba38e6d57cf238362d4ac568823b1fb75e20f75b58cd062a3da4d9a7a/backports_zstd-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a68ab446d007d34e12f5a812e6f7d1c120a3d15cb5d4e62b7568926a6da6fb7", size = 340429, upload-time = "2026-05-11T19:54:05.632Z" }, - { url = "https://files.pythonhosted.org/packages/e1/bc/146fdb7b0bf39817e1b706e34be46f2cf11d5465668e1912747dd45fd71b/backports_zstd-1.5.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:627973d4375a42500a66cc2ea912f6223249a6cdfeb56cc340b0d20b5a3475d0", size = 421477, upload-time = "2026-05-11T19:54:07.499Z" }, - { url = "https://files.pythonhosted.org/packages/f4/2e/6e43d94a3414d0113439c5e9ae6b04311797cfef5d04dc1d3aa0bcbff057/backports_zstd-1.5.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c077639e99de02a679dca9c6a189f60a76e7d0096977c0ebd070c31de8df57a", size = 395021, upload-time = "2026-05-11T19:54:09.171Z" }, - { url = "https://files.pythonhosted.org/packages/b1/41/d599f31e5152f43397f837c6911bffee8626d6d079bcaafab04d1a8a07ad/backports_zstd-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ac2b3895fc9b1f0b0e71bffa179b48930dc27643b7e4885869afd295e7dfe1e", size = 414986, upload-time = "2026-05-11T19:54:10.986Z" }, - { url = "https://files.pythonhosted.org/packages/26/62/006a63d5a13a04384b9cd35e35f78944a75c975f5a71c25e81cc766d53d7/backports_zstd-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41b23cbd72f503aedcaaaa23d55d2d98d449e5938154d2b3f57832c73b286cee", size = 300853, upload-time = "2026-05-11T19:54:12.593Z" }, - { url = "https://files.pythonhosted.org/packages/89/92/8e8769e1e3ebec16d39f455e317a0f137a191b1f122853d0377c660666ce/backports_zstd-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0ca2d4ac4901eada2cfb86fda692e5d4a1e09485d9f2ec5777dc6cd3154b3b46", size = 410809, upload-time = "2026-05-11T19:54:14.117Z" }, - { url = "https://files.pythonhosted.org/packages/63/5c/741a2923020c45b85cad4dffffcb86dbfa2d4aaed27f18ee793428ef4c24/backports_zstd-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:20796211a623ec6e0061cef4d7cca760e9e0a0a951bb30dc9ba89ed4a3fea5e4", size = 340342, upload-time = "2026-05-11T19:54:16.165Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3b/68c4fe8a551d3f47ed75ddcf15dc7c777bb9d869fc0e0f5b7cacc9f158f5/backports_zstd-1.5.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:5232cd2a58c60da4ceb0e09e42dbc579b92dda4a9301a756af0c738223a23487", size = 421476, upload-time = "2026-05-11T19:54:17.709Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4d/ab5dcd6ab9a7ac02ec42c4507211da7dadb9498abb655115c296077e2b8b/backports_zstd-1.5.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:012d88a9ae08f331e1adc03dfbda4ff2ae7f76ea62455975827b215677a11aec", size = 395020, upload-time = "2026-05-11T19:54:19.566Z" }, - { url = "https://files.pythonhosted.org/packages/55/aa/ec512a0d14552bbb4e75693f7065434b865956abd045ceb67f0574146241/backports_zstd-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb7d79f8e43b6e0e17616961e425b9f8b32d9933e1db69242baa6e21f44a978", size = 414985, upload-time = "2026-05-11T19:54:21.136Z" }, - { url = "https://files.pythonhosted.org/packages/aa/31/759d077aa680555e17c9d2bb09edf4c3428d895fe5d35a8df67684401b84/backports_zstd-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6172dcdd664ef243e55a35e6b45f1c866767c61043f0ddcd908abd14df662065", size = 300853, upload-time = "2026-05-11T19:54:23.1Z" }, +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/b5/5a873da082bd08acd6a497f7aae224e94a7c27fa8f24488089cc50a16c84/backports_zstd-1.6.0.tar.gz", hash = "sha256:80a7859ffe70bf239d7a2ce15293bdeb5b4280ff7dc326ffab312b0e254dbb24", size = 1000009, upload-time = "2026-06-14T10:50:58.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/8d/3f8e7a0fd319b3c0dbf0c4f751336309bb50a873b9185c2f5d228ff0d21b/backports_zstd-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:73000459db113a658c4fb0510100ef0e79137b5828bf957b7709aacae4eb1b87", size = 437068, upload-time = "2026-06-14T10:49:05.528Z" }, + { url = "https://files.pythonhosted.org/packages/db/14/4700047713a60131efcb3977a9892fab60bc9dd6634272550b8f1c5a427d/backports_zstd-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6e78d5e28f812b39f92397806ecddd4a6f3bf35531a8c039a1f187abc931af8", size = 363456, upload-time = "2026-06-14T10:49:07.154Z" }, + { url = "https://files.pythonhosted.org/packages/c4/34/92de2f1bd5ee29b24302c871b9f3c19155bf9478cd3af5a0dfd70fa2f483/backports_zstd-1.6.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:32f04d54ec1fdf3aa648b24a10b1c9234ed2046cc4af7a8850cbc236c05d42f3", size = 507392, upload-time = "2026-06-14T10:49:08.63Z" }, + { url = "https://files.pythonhosted.org/packages/5a/95/ed5b8b026c6df1a59681a73396f63cfd10e17ccfbc6315974745a8b7d834/backports_zstd-1.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83415af3c64550a56cc20b4cce59bbaa81f21d28466d7adf98feff011ecbc66d", size = 476957, upload-time = "2026-06-14T10:49:09.894Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/1a82ede48d9df99c8245cb38622cd1a9b388b34f89e1cb7b6650913b493d/backports_zstd-1.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3c17e6a267d13de9cbf14bf2ebfa87e03d26692456fc67d2dbed9da4f479b18", size = 582618, upload-time = "2026-06-14T10:49:11.097Z" }, + { url = "https://files.pythonhosted.org/packages/6b/70/441ed36e230b0f66d7d49382c58249b540139c5b1aa096ace1ff00bd7873/backports_zstd-1.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:75578c71644b031118ce938855a53530708db7f4af6e83e2f8840d5a1de990f8", size = 642278, upload-time = "2026-06-14T10:49:12.545Z" }, + { url = "https://files.pythonhosted.org/packages/58/a3/8f5737bdb02576577a018c10a4c345a5b4b2e63cc3811baeecc054f71c00/backports_zstd-1.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4ae7ed5a6d813450cc2d818284ea3db9721edcef50a56aae42ea06feec38c6e", size = 492492, upload-time = "2026-06-14T10:49:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a9/c086507f535c2466a25bf83a876666c9ccde07b17ce81680217ac17355fe/backports_zstd-1.6.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5e9a8370c8ed873083d5de956d6b2e60adbad31e52d7a11111c96ef01d1910ae", size = 566440, upload-time = "2026-06-14T10:49:15.483Z" }, + { url = "https://files.pythonhosted.org/packages/89/bb/778aaccb58c4d2fba3482438c0d33c6a3a413710ecdb2ee8559ff28632fc/backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c2d1ccfe088e8279d605011a3575619a74526c261be357695b3258c0f636115a", size = 482894, upload-time = "2026-06-14T10:49:16.982Z" }, + { url = "https://files.pythonhosted.org/packages/3a/88/87ad188ce971c15bce933e13c4dc2939e741a9cb06a8bd692ef8614c3ed4/backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e73a550dbeb84e8fa50f8385f7735e9a4735b465851ef617d02f80ab10e44e7e", size = 510822, upload-time = "2026-06-14T10:49:18.274Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8c/01714884a14b836abfdb1d80339acbb39515b0e92e615c4b65caf0eec257/backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:84f92e5a60a78c72ccda79d0417d311a1f6da18f446423ed411726d545bf7b56", size = 586941, upload-time = "2026-06-14T10:49:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/63/43/e2cb44bbe3f7485d6ad8493211f0c8fffdb7e02fb94203fd968751d9fad7/backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0eb4281f402b94d397b7482f6d9efd04c28274e4ed6eb57eb1f87bdd091a6a87", size = 564255, upload-time = "2026-06-14T10:49:20.968Z" }, + { url = "https://files.pythonhosted.org/packages/36/eb/eb0f00f6f7778db3710f757d77f5699c325548037dd975b52b186394125e/backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d6b9b06323e3ba947c0003b2d70e02f33c90c36bc6262a92eb8201afc4a1aa08", size = 632836, upload-time = "2026-06-14T10:49:22.177Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b8/5434897431de92e79ebfb2c02e1ab3cd228e92853b7ff981b2cffcce7355/backports_zstd-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8872a0e9f1af975966b5be6af7eebd3dc4046f15e470b719316516dc3d137cd6", size = 496501, upload-time = "2026-06-14T10:49:23.444Z" }, + { url = "https://files.pythonhosted.org/packages/db/76/754939c3914e9724e20a50b75b17fdc27aeb24d697eb61c3e93438a42920/backports_zstd-1.6.0-cp310-cp310-win32.whl", hash = "sha256:c14fa5dc39a804f1b92d63506f450eca5c59647a18d197d1a564b89dac1be1ce", size = 291527, upload-time = "2026-06-14T10:49:24.568Z" }, + { url = "https://files.pythonhosted.org/packages/84/f1/adcebdc2caa2e3d3d496ac2501f0ada49ad2942ee36e5f88a944bca9ca92/backports_zstd-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:8219d6fceae6b39535c4ac323dba0923d10f781d59962ff3504e693fdcafa92c", size = 329024, upload-time = "2026-06-14T10:49:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/74/10/12edc0b401a08aba157b9d331748ac0f0e9890af0a58a9c72425063d1450/backports_zstd-1.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:b7bc9a0b66097f03820a54316d2fdd0beb38859cf98f10d63e94c55450ed8920", size = 291597, upload-time = "2026-06-14T10:49:27.156Z" }, + { url = "https://files.pythonhosted.org/packages/c5/90/428dd82228b1b6d62d5a1bf312c29e6c125af6a182fcfd82768ca179dcc7/backports_zstd-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4fc41b2df5529cad5ceb230319e82728096d4b353ce8d4df68a2ec37e291bb8", size = 437067, upload-time = "2026-06-14T10:49:28.335Z" }, + { url = "https://files.pythonhosted.org/packages/ef/48/768edf21fe33bae8d874470b1be136681d4d32eb820a32e1c98262ebe39b/backports_zstd-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:83391ef5935cc0f329b1abca414ae20ffe40d335fc21a4b5e664f08a74317d5f", size = 363454, upload-time = "2026-06-14T10:49:29.784Z" }, + { url = "https://files.pythonhosted.org/packages/29/8a/d462c2e5071eb573378f0d26760f6590613086fdf59c2d3c66bdfffb9f41/backports_zstd-1.6.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:7d3f64c503af7b60115b97c16feaf75bd191ef2c978d5c0c7725a6682bef63c5", size = 507393, upload-time = "2026-06-14T10:49:31.077Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cb/af58363b0dd0b497282ecef1fa99789b03cc1885a01a41394cad42ceeff6/backports_zstd-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0308990ffc998df3c7ed35276bde049728b5c3956203cae40d80893576a41459", size = 476957, upload-time = "2026-06-14T10:49:32.53Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fd/5fbdf2275cefae95c4b3509f6db2dc372d0587ebafea342d28781d51d932/backports_zstd-1.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c298785e2fadeab82342040f2d9ce764ce500e6da6a6d99a2de514e63580b5a", size = 582618, upload-time = "2026-06-14T10:49:33.723Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/7dd45c53c907ea67f635c3900b58bb3347c01dc2ded441402028aae0ef9c/backports_zstd-1.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae106fe16e36efc60ab098d02478d30aa0e31e1420eb4ecf0116459253bc6361", size = 642279, upload-time = "2026-06-14T10:49:34.938Z" }, + { url = "https://files.pythonhosted.org/packages/4d/25/a9e37dd035027565fa0b7e367da50e88a6ab26e7fd413269aa118e25258b/backports_zstd-1.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7293fefe15f0e5852bdb4ad1e0e26f3cbd4d3e61c19f751ecc4ff34bc1eb237d", size = 492486, upload-time = "2026-06-14T10:49:36.06Z" }, + { url = "https://files.pythonhosted.org/packages/a1/52/659686bf8f7c53ea279e1c44038504b82a6901cee2f5ae83c30bbf581301/backports_zstd-1.6.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ece8e7288db5b827ef8c64b2f78519f1a173a8991a625978fce02eccd7654fe9", size = 566440, upload-time = "2026-06-14T10:49:37.536Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1a/c7ea5a0ff607a1a6066bb7c7cb65ae20e2f85da6adc69ab77fd8943e180c/backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28eef3881164f3c23ce58ed59e4684103bdd279583eb2d299858c9e9b72fde9a", size = 482899, upload-time = "2026-06-14T10:49:38.805Z" }, + { url = "https://files.pythonhosted.org/packages/83/48/bd2b91100ee4fe6bb4d816e3659cbbb0cda5dd32760d2379c54d1752ec25/backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:481a1e9bd8f419fdc625307aa20234687f99368c75df511ef589693c5fea4c6f", size = 510826, upload-time = "2026-06-14T10:49:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/fa28509d7ce2ad59404e7ce738a2fd858e12dfd9a896629f10330222a7fb/backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3b6713371f8987a1178df93cb36f29eef191f224021e2d656b2f11ce60d26816", size = 586941, upload-time = "2026-06-14T10:49:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/45/28/757daf2399aa71bb37f9f7f48b42ab03fc51c340eccfad2fec92a23f6aa3/backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b0ddbcd2866b8ff1a2836e4b0e4d44788f5b992d83fac75a38cda8f9a2bee079", size = 564261, upload-time = "2026-06-14T10:49:42.49Z" }, + { url = "https://files.pythonhosted.org/packages/4e/53/9b9db30cb2c148a69c40ad7647aa787338041f3dc81c5b22113286e590e9/backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2914abea516704bdafb2090acd3f15b5f9debecfabd15b8dd8285b2ad3b92209", size = 632869, upload-time = "2026-06-14T10:49:43.981Z" }, + { url = "https://files.pythonhosted.org/packages/81/a4/1692fbb88af8aaf900a53619fcc95c9e45d9ff162223a47fd672a9893c8d/backports_zstd-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd085eafa2aac6f883afd28210a3231f717f25409a1e44a39bb7b04c8c5b5646", size = 496496, upload-time = "2026-06-14T10:49:45.118Z" }, + { url = "https://files.pythonhosted.org/packages/93/42/c5a66c47320bd12ce84a7341330ea582d67069bdb70214bca0b6bf394cfd/backports_zstd-1.6.0-cp311-cp311-win32.whl", hash = "sha256:b81b4cf3d6e0ad7ac92bef248f49fafc954262c5fb0f7e19d6aac497e5a856b2", size = 291613, upload-time = "2026-06-14T10:49:46.473Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f2/f22c19b4cdde429805ff5ac8dd77a95569a7c4cb8991741b2ff0d538f220/backports_zstd-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:10b61850c4112952e05aa6e6cce8c9a5936fbeadb321e154216705cc76a14afa", size = 329078, upload-time = "2026-06-14T10:49:47.71Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/e902a3f1eb92c4907b5f47f90cb3c2734ee315c4ff67179fc111343b45ba/backports_zstd-1.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:068ef3d8c18815a2e3a752f766313e19910e7c50939b956923748d9c04ebcb1b", size = 291727, upload-time = "2026-06-14T10:49:48.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/009af3a9532d4cc66d5385391c512210fae32ab2442605f26aca1d8d2957/backports_zstd-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0466b14723f3b7697669c00ee66fe16e30e25636b286b0a923fa86fa3d8a753c", size = 437407, upload-time = "2026-06-14T10:49:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/0c/76/f7c02efde81ebb9993586f9e435d2fd1191a6f806f640e4eeb8d004493ed/backports_zstd-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d146926e997d2d3de8212bdcbf4985344a2622ca3bec458d8908000a84fd883", size = 363519, upload-time = "2026-06-14T10:49:51.383Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5e/0cf66f12472fe3e082cc4134395a7e0b8746cfb30aabd74251ce8fafa9a7/backports_zstd-1.6.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:460fd6b3f338c659507ae36cfd6b58ac9942a2ff233c5cf574416dfec0451a84", size = 507756, upload-time = "2026-06-14T10:49:52.497Z" }, + { url = "https://files.pythonhosted.org/packages/03/95/7ed25c90369360f96f8bfa961540845e063377c32a43b775201af66a588c/backports_zstd-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c2b1f4a640c51130caa92cef5bf72bd3c3dbbcfbf814c37403aa0601b1811b0", size = 477578, upload-time = "2026-06-14T10:49:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/e3/75/f16b1d3e33ca396525847c81d96e3de7bc74d2c6f9ca2ddee76b0c450697/backports_zstd-1.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:beb43e9885202c8d4f3762319ed4d5e98e197622afbff8439fbbdd81d08938b9", size = 583029, upload-time = "2026-06-14T10:49:55.132Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2b/a17b111b631e1c79a0e570881c1a266c661b936585afa395435a458b1991/backports_zstd-1.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fbb746522ebfc11155f1cd688e2c48ef3d74125e38b63eabdaab068a055c3e88", size = 641741, upload-time = "2026-06-14T10:49:56.42Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b2/d17b2722c636d64b4e77ddc68d8d0625719d39f94021be8719a218af4c0a/backports_zstd-1.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a99710fbb225d459d66def4dc2bb2cd4a9a0bdc8b799fc0621cfdd863be9c93", size = 495554, upload-time = "2026-06-14T10:49:57.652Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/2853e8b6c03f03795b6548ea61f82cc104d4f7ff2523a04bc69f46984663/backports_zstd-1.6.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f69365ee2b836939137de024a302395a1cb8654fb6dc5ffef6381105259c8f87", size = 570027, upload-time = "2026-06-14T10:49:59.003Z" }, + { url = "https://files.pythonhosted.org/packages/18/aa/83f37b81f3b8c6ea035bf260ec374648bd59372894c02323dc9de3cbdf77/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66cf8038893c7708ec345ffb3ac63c775d10f430f323ac2f0334fdb6a397c57c", size = 483594, upload-time = "2026-06-14T10:50:00.49Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6a/d77f8cd2ff642d3b3652c1ccab5b6583114dbf10f8cb0143531357c83998/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e514c71ca72f3b56bd8fbda1a6a5b7d1100a2764b42a3c74a38841f25f9b00ab", size = 511206, upload-time = "2026-06-14T10:50:01.86Z" }, + { url = "https://files.pythonhosted.org/packages/56/b2/99a60fe4d1aac8053769d2463271d5df37a7c11c387072fdbb0b16aed7f7/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7741e44f7938ec94f9a52678c8d19b7bc548522ffdc39c9e4481af8db545fa9a", size = 587416, upload-time = "2026-06-14T10:50:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1e/a9c003fe4d14bd4bf671598d4c7dcc1cef51e3513d9d7111ba1d07b6f07b/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97e8a9674652496c7612b528085dd5a296c052a2edc466ca1bfb7b0b27820413", size = 567615, upload-time = "2026-06-14T10:50:04.524Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b9/955bd604f692c550c7cb66d00bd7691ead5c86df8ebd23d7254eeaa90789/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:23a793f2fed4dbf0517319759a2cded0b0dd8e8d3797fe30badd5693e320c175", size = 632269, upload-time = "2026-06-14T10:50:05.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/9f61f612f8a4193484c78a1f26db82a50141234189885113ef0085a8a961/backports_zstd-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b951113113ed4b8d173418a4f155c14b739dace626b3fa3f82be1831958d39e4", size = 500066, upload-time = "2026-06-14T10:50:07.446Z" }, + { url = "https://files.pythonhosted.org/packages/81/a3/19fb8c48d94139481c5ccaf2fb54c31b543fa635fd7bd7399aadd15752ac/backports_zstd-1.6.0-cp312-cp312-win32.whl", hash = "sha256:6430b34a2ae6fcc604672f4f913102563473d9a015bdca1ce8c95041cc1f2677", size = 291825, upload-time = "2026-06-14T10:50:08.762Z" }, + { url = "https://files.pythonhosted.org/packages/58/38/40ba081c6c71f0f22c64d3d54b912ad75a4e6812caa1397cbb15b5693b12/backports_zstd-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:08793876172551a930ce4d65c712cd516184d1a97070d4a1193e05bf0cf7040d", size = 329201, upload-time = "2026-06-14T10:50:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6c/f7116dd2edc6f960545f0d8616939eae3a20031b3b6669697d4f9fd83b2e/backports_zstd-1.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:03b7c59c71f7a597e2bcb3f8368371e9a660a1bdf1c37afc1f1ad1496a013c19", size = 291901, upload-time = "2026-06-14T10:50:11.198Z" }, + { url = "https://files.pythonhosted.org/packages/38/06/c430537d59c55d49bcd15ecf4b1aa965453219caad810a4f2b484816f4be/backports_zstd-1.6.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:2ace939e4d620e119423606f2d3d7115f8707733bf57f279ad9a9383f875986f", size = 400327, upload-time = "2026-06-14T10:50:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/36/48/2f8323bb0e3ebba88b54877a2979afeb83983fb2ca572f09ad61aae2d3a0/backports_zstd-1.6.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:4c68a9ed2df0cca51d774c521e68a34d2e3d9ebfc687ef8096adfd4f345b551d", size = 454276, upload-time = "2026-06-14T10:50:13.667Z" }, + { url = "https://files.pythonhosted.org/packages/7c/39/87a665244a65f5b87a06b848c29a8cce07e91d59c5988ee2a32c0293a21c/backports_zstd-1.6.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:30576f49b82328ec8af16c11100efe52ca88526f71bbe100ef6b4e707dc13bf2", size = 357457, upload-time = "2026-06-14T10:50:14.906Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8b/854d4a47bb8b7a48bfb2ed381c7b03a70efb4fc49f0e4a1509b38a2e1727/backports_zstd-1.6.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b4bddfcfb6679215d6f4dc5f79a1f9301af339480d70527a14b57a1f2e6b6cbf", size = 366139, upload-time = "2026-06-14T10:50:16.399Z" }, + { url = "https://files.pythonhosted.org/packages/8f/de/c3af43eb8df6f2581e157e18a3e0121eadb826055b2fde3f91ec188689cb/backports_zstd-1.6.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:65048ed08c5124f05ff9f355ab9703014bb2dbe7f8d9948ce193685b1775f442", size = 446683, upload-time = "2026-06-14T10:50:17.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/39/87cf3d883d386c10ac52f5322604fb9afdd204229f4c47d4a820a839b8ff/backports_zstd-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5918fc6b31437208721276964323933cd86077b8d5b469c59c1b3fd2c8220a05", size = 436869, upload-time = "2026-06-14T10:50:19.113Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b6/9479e6f0f18824ad38e8d7dd85161ab0842a198be669421232925bb30960/backports_zstd-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b6c8b02ab0ccb2431bb7bc238be91d158b308915e7b07937388e540466fe7e7", size = 363090, upload-time = "2026-06-14T10:50:20.302Z" }, + { url = "https://files.pythonhosted.org/packages/d9/74/a5e98fe108e17c91d9bc590a19e77f5d47d579e34d3f5bc098a949d6c27c/backports_zstd-1.6.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:711e6b98f8924e8b4a61ff97ab6321f33de024e1ed6a32f5123763aeda8459be", size = 507070, upload-time = "2026-06-14T10:50:21.536Z" }, + { url = "https://files.pythonhosted.org/packages/69/f5/392bb7dce7363b77bc5403060f418fad438b9cfdd3edd10d65cee7d8fd11/backports_zstd-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ba9ac10fc393e5123a08802e0e895a107cb4a66b9973d2844dbd8a343111e59", size = 477200, upload-time = "2026-06-14T10:50:22.91Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4d/dfb665806ba4f74bc48071d32006843b53568c4a17ff627a3061de5eaa09/backports_zstd-1.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f723219335387d7546412d8141e0303590600949b4184a1391a0c6a3c756058", size = 582724, upload-time = "2026-06-14T10:50:24.28Z" }, + { url = "https://files.pythonhosted.org/packages/57/b2/beeca7393a8310debd82ee2f0ce5c1801e8d7cb673f7f226f4a0866ca238/backports_zstd-1.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64b94d7a836568926a3309ff510c7f8261b881b341fd4992cabf4f0998878f8a", size = 643493, upload-time = "2026-06-14T10:50:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/38/26/ce90e9eed6f25aaa4a4fa305a2aaf2d2ad81fd69de8eb248ddd91c80d1e0/backports_zstd-1.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e39258a09b1c7ca70b5e94a5c5ccfe4700b4250b8077cfeab31d0f79565d4c9b", size = 492190, upload-time = "2026-06-14T10:50:27.205Z" }, + { url = "https://files.pythonhosted.org/packages/17/9b/37b9b146df1f5452419a96071a7017cbac212ec9b137d7a88ca46dc2aa9e/backports_zstd-1.6.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:15b1aae0f64cd742df4bba1d989d0a09a6ec619202543fdba684640454541fd3", size = 567432, upload-time = "2026-06-14T10:50:28.386Z" }, + { url = "https://files.pythonhosted.org/packages/06/66/81b30991be83237529f36335ac3682bce26409064b906ac6122874575196/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25b5ddc789480072551af571a746e9500356b2aff0499861cf2ca07ea7431e68", size = 483021, upload-time = "2026-06-14T10:50:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/49/2a/792c65dcc1e45eb0c1bdc012ee94b84867186bfe27a860d0813bd216f03b/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a13cfa3410a75e4cb87abdb669aaf79da861cb79299159054ff8f77b9671bc40", size = 510596, upload-time = "2026-06-14T10:50:31.657Z" }, + { url = "https://files.pythonhosted.org/packages/1d/22/01b92a600505620e4cb5f20429e181f30458b7207ca8b52ca5ca6068c35f/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2ddab55a5f54dec8acfad68ef70f1c704fd21919990ddc238afbd6f496e61c6a", size = 587143, upload-time = "2026-06-14T10:50:32.868Z" }, + { url = "https://files.pythonhosted.org/packages/d8/60/4672f5110b9eb01388cc6225a739e3a5fcd749a63a9c4c1450a04fa27113/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fa305a84087e10d7a85e8a8a3dcba8cdbda4868f2180173b264b7b488fd37c55", size = 565238, upload-time = "2026-06-14T10:50:34.173Z" }, + { url = "https://files.pythonhosted.org/packages/5c/3b/19928d60ea7d25820bf12ef88de74534ca85b56ff7cf13c1b0e74e3a3d7c/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:df27b57d214a3124fbe4e933ef5a903d4567f154260d9aece8c797a987f2a205", size = 633970, upload-time = "2026-06-14T10:50:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/df/97/c4cecb3e0ff53563ef9819f0395d919ceaae9c5147392ac23bac7afdb20f/backports_zstd-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:28fecd73459d74910ae1987ab84b7bef690d3dd860948430dd5555108b006daf", size = 496539, upload-time = "2026-06-14T10:50:37.015Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f4/46b2f29d2938a80e56e61a19f11ab093f531a9f8cd0ec8eeaac1246bcd99/backports_zstd-1.6.0-cp313-cp313-win32.whl", hash = "sha256:3e689af303df287142770abe3a48bbefd24dab4a09da5807d0e1fa8c75bab026", size = 291451, upload-time = "2026-06-14T10:50:38.518Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ad/b529f92166da61f496621345f95d2dc583c8ca5ac553c084a4ef6c12cd71/backports_zstd-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:b067b1ef9c8e41fb0882c828aa37829938b5c0dab067eca72b23fc24c563b9da", size = 329023, upload-time = "2026-06-14T10:50:39.742Z" }, + { url = "https://files.pythonhosted.org/packages/30/d8/6be904d20345fbebec583ca83676e01f30c76118b283eb666d8ec8291ca1/backports_zstd-1.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:a838296f5b84c920172fb579cac894d255c1fc25457c7234613ddcfa385e49b7", size = 291636, upload-time = "2026-06-14T10:50:41.004Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/9ed88f528b9484f3847f07b9d1d014b496e048d391b4bc04cb0117bd71a5/backports_zstd-1.6.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6c73ae37dbf9207727ac095dedef864c05d836eaec962a47b3b64eaadaf1c6b6", size = 411126, upload-time = "2026-06-14T10:50:42.253Z" }, + { url = "https://files.pythonhosted.org/packages/fe/26/bf8093d117cb6c36202ee7a2127f672c7b0c81f0c104ce28124534b75efa/backports_zstd-1.6.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:839faf90a7eb525a401978dc925df8c44bd12526e8ba1529b9f8a7106e729637", size = 340643, upload-time = "2026-06-14T10:50:43.571Z" }, + { url = "https://files.pythonhosted.org/packages/17/10/55f0860ed359d290e0eadd410da47ae720a1acf0d8362149e22acbe63223/backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f8f5c1c7c69a4b00889e52d9304a918a5b49010f9645768eb5fd0ad404f790ba", size = 421696, upload-time = "2026-06-14T10:50:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3e/8dd9cc6f3e697e4721e53d6b9ca8c95c9d51ad2e759e7fcee6885e5b71db/backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e80bceebc9b58e959bede9b26cafe15b5b9526f3533a6dd06330c5da73cb9329", size = 395239, upload-time = "2026-06-14T10:50:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/ab92f8599749cb6063416dd9f46e084004c4e8db68e2eb768283012a6d27/backports_zstd-1.6.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79284c1dd702f4f24ed1a36e51555c907dd237b6c0d829595978f4089a2aeea9", size = 415202, upload-time = "2026-06-14T10:50:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/dd3ef995f6b22e23da145ac3ecc91e1f1fc4cb572b7f95e6b2b11de16782/backports_zstd-1.6.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1e20b3ecd0a711be82e964aca28554eabbc31ee69a20e5e7b8fd42268af46212", size = 315722, upload-time = "2026-06-14T10:50:49Z" }, + { url = "https://files.pythonhosted.org/packages/e8/09/898fe2f8196fa7ab825f5fed786c68581fdac7d23a8e20baa0cc01cb2f0b/backports_zstd-1.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:aeef8563b82ed4af328f98e5041c1b4800d86f68f857ffd1577d4d47dc9aa6cd", size = 411023, upload-time = "2026-06-14T10:50:50.286Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ad/6ad9af1596ab5f284bb53954be41396e13d23c81cdfe3d945402e8ee0215/backports_zstd-1.6.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb75e33131946fabd6319061df3b8b1d588fe0963183280e9b5f49f7772fc09", size = 340554, upload-time = "2026-06-14T10:50:51.523Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/f083d7c8a4ee5d0bb21b4d3144e76de9f655ca4dd0bffcb95baa5bc47a62/backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:ef132cfb638e9a86bd5dc07fb4e1cb895bc55bce6bb5e759366e8b160d0747e2", size = 421694, upload-time = "2026-06-14T10:50:52.917Z" }, + { url = "https://files.pythonhosted.org/packages/41/d7/693b20f3ccae2e05d166f98fe55b1657451170b72c804ed9f6b98df520be/backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab70eace272d6f122b121c057e436709b50a28abf30d97aab28433c08f4a4095", size = 395237, upload-time = "2026-06-14T10:50:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/53/a1/484e0f9ec994bd2285d6747e7c8028350f1a177e9210bc57637898042d3b/backports_zstd-1.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17efb3d11137de5166dd51eedab9c36ad633402acba386eee8d715213ea47e49", size = 415201, upload-time = "2026-06-14T10:50:55.854Z" }, + { url = "https://files.pythonhosted.org/packages/3c/56/70860ece85cd49b564305cbc22bf6c4183975427ff6dfe2097e855f5dd5e/backports_zstd-1.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:994167ff6551b9c1ce226e0aab16295b98c94507b5701aa60d2c32b7d50796b1", size = 315721, upload-time = "2026-06-14T10:50:57.074Z" }, ] [[package]] @@ -184,14 +184,14 @@ wheels = [ [[package]] name = "basedpyright" -version = "1.39.6" +version = "1.39.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodejs-wheel-binaries" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/1a/48296b4479ccc9051eb9617a6507a69a68f5b68693fb6a118cfe08199270/basedpyright-1.39.6.tar.gz", hash = "sha256:d00ec5f8ba4e1a67dfc2fa3a9474229c89f61f207d14c02d320db78f57aa16ef", size = 25504244, upload-time = "2026-05-24T07:44:41.864Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/4b/c1f4e211e50389304d6af32b9280e026a7133e3ad59bbdf8f7a3250f8bee/basedpyright-1.39.9.tar.gz", hash = "sha256:32cbea5fc8273e89df3db20daea56cb7286e419ccdfdc479c64759d2dc071901", size = 24412216, upload-time = "2026-06-27T02:19:49.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/07/6d1b3192715d42e8c9887876684a941eff28ec5d79c23a0f3758377e2182/basedpyright-1.39.6-py3-none-any.whl", hash = "sha256:5e0b9befbae6b26d0fbcc6645ac26923725e749d1224539e24f05ab07f9365ad", size = 13182122, upload-time = "2026-05-24T07:44:47.086Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d4/e1fa108710d0498a18c77b1e13897f31eab47c69aa8cfe2d2a4df746541e/basedpyright-1.39.9-py3-none-any.whl", hash = "sha256:6b0837b9eba972c71895167ab9b127e6afdbc17abc92312e3f8d15ca82a5611c", size = 13374276, upload-time = "2026-06-27T02:19:54.431Z" }, ] [[package]] @@ -279,11 +279,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.5.20" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] @@ -475,14 +475,14 @@ wheels = [ [[package]] name = "click" -version = "8.4.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -505,74 +505,94 @@ wheels = [ [[package]] name = "complexipy" -version = "5.5.0" +version = "6.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/39/a390aa5acccdb5df51c0fbb8cc3b5feba94d86596d78d1257c3624adc7fb/complexipy-5.5.0.tar.gz", hash = "sha256:c6d85ce28c0e8257a0a2caf644b51094438750ee34d6140f9da8859278b70fe5", size = 346876, upload-time = "2026-05-22T19:41:12.198Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/33/d92e5b18aaada5b664811473970200def38182746551056479fd7bece07c/complexipy-5.5.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8e956ac59623adbf4dba312adefb238459d8e9b0591731d4cc692df531c057fd", size = 2215322, upload-time = "2026-05-22T19:38:41.484Z" }, - { url = "https://files.pythonhosted.org/packages/7e/72/c51d960e91f537d0a6943b59594222e07b9b179ccf1808d53339e97f8d9c/complexipy-5.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:576fd72bf7c3959cae04107d056913ac5a7027adbc312f008d271c7940fa7193", size = 2134395, upload-time = "2026-05-22T19:38:43.369Z" }, - { url = "https://files.pythonhosted.org/packages/17/a8/7b5bcebd16c3075033b2f517d9abe2a5308f5487544a1f9656951b355711/complexipy-5.5.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f11775da7f12015d30a353554924841ddc60b528e79311ee06c5be6a6c2ce57b", size = 2323616, upload-time = "2026-05-22T19:38:45.392Z" }, - { url = "https://files.pythonhosted.org/packages/4c/be/94a3776d51f104bca21a73f273438dea552db525cb5f5379327aab5df0d6/complexipy-5.5.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:801fd1bbb07599f729612d08ffd49761554086dcf6d90b9ee8f43f9fab3138e9", size = 2255157, upload-time = "2026-05-22T19:38:46.894Z" }, - { url = "https://files.pythonhosted.org/packages/29/64/40b8ade92f60adb5541b1ce54b3a315cc48f1bc34a35b8ea05fac786bf73/complexipy-5.5.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:78a67d2e19851e691660ea1b7244c31c925c21e5b038248b152eb63c73027afe", size = 2456230, upload-time = "2026-05-22T19:38:48.909Z" }, - { url = "https://files.pythonhosted.org/packages/c8/14/773832bdc36250b5d821dc368fa777ae609cba1f32f8be72d4fb831d431a/complexipy-5.5.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c1b63c75c256c679e7ffad451155c0e3e95aa25bf0c43a03401d668afc7eacb0", size = 2709811, upload-time = "2026-05-22T19:38:50.806Z" }, - { url = "https://files.pythonhosted.org/packages/00/f3/3a04d7f1d268f78811399c13e40193893c6c39981d719bd70dd703d4bb81/complexipy-5.5.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f08f84a5027279d3d9786cc5b85dc6c0de12d4144e66d7ae927e341f52c4e868", size = 2474659, upload-time = "2026-05-22T19:38:52.645Z" }, - { url = "https://files.pythonhosted.org/packages/5e/be/7923b36eff5239b71df0f6acabe15e9cffbf755e379c80799dc62f5524ff/complexipy-5.5.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:441e8bc484ca9a90c347a01af0817fcf60a032625b8d0b6743bc30e10761f7f4", size = 2378383, upload-time = "2026-05-22T19:38:54.334Z" }, - { url = "https://files.pythonhosted.org/packages/85/1e/07a16ce1b1e8dcae62ffe856e099f17fc2b84fd232c41c8a04dd3186d87b/complexipy-5.5.0-cp310-cp310-win32.whl", hash = "sha256:3fe92c5af3407c268f2ca22c105528f8e2f1d8f10f0d0eae2e9dcdcb34320b7c", size = 1915101, upload-time = "2026-05-22T19:38:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/4c/5d/8c62004906ddc60ef6c46a16786bfe443692cea3e8a5b8d27d47f6d98c68/complexipy-5.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d2796665ac80b987011d866b8de2468f7a8e4b3d25c81f856de1fc663285187", size = 53064, upload-time = "2026-05-22T19:38:58.595Z" }, - { url = "https://files.pythonhosted.org/packages/a6/42/aa85cd651876308f942098e9fa52e8079105844ce57ca0bad3f0a05cecfa/complexipy-5.5.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5cb84ed46a9d77958c4ae8d3251019f5ac700bfe764b929bfb51dfcae8b68a00", size = 2215335, upload-time = "2026-05-22T19:39:00.151Z" }, - { url = "https://files.pythonhosted.org/packages/62/fb/de47404bede2be38592aa362f6f3c831ac827a1a18c233fe5b42cbc39173/complexipy-5.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:790ccf2d23bd9993545ca547db19b269217f6699ca271042b6117547b18cc78f", size = 2134372, upload-time = "2026-05-22T19:39:01.976Z" }, - { url = "https://files.pythonhosted.org/packages/17/0c/b63fa0230f47f0486234d1719eb71f42deb3d0dad4df93b0293c1f61f7e5/complexipy-5.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b337566eb6eddfa1bfec845c51dad99539cbbf7d7a7d83d90b6f3cb15d34e74a", size = 2323679, upload-time = "2026-05-22T19:39:03.89Z" }, - { url = "https://files.pythonhosted.org/packages/46/a4/bcfebe60935b7b1b446efcceefef3df34ddc2b92d1425fa36d47887eae14/complexipy-5.5.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:7daae67e3a830a76e925301428d9d3fa1401ca4dd7178568d043928f66f96051", size = 2255221, upload-time = "2026-05-22T19:39:05.432Z" }, - { url = "https://files.pythonhosted.org/packages/ef/72/1795b2694a6b0ccf4ad9f79f8612774fd09c07e3a64746adb7846e87365d/complexipy-5.5.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8f77e3acfdec6abcae68fae9f83ea1f608956f339242c458f827d3dac7ef45cf", size = 2439420, upload-time = "2026-05-22T19:39:06.852Z" }, - { url = "https://files.pythonhosted.org/packages/0c/17/d2f8ce1f2c69beb91fa3da59420ab32ccbf88a1da375b4be984a811c11eb/complexipy-5.5.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db58c46350ecf3942704dfbea77b4a1884c21f63a6c5a7fe83e624735fce8248", size = 2709880, upload-time = "2026-05-22T19:39:08.996Z" }, - { url = "https://files.pythonhosted.org/packages/6d/bc/dc0be399529dd9c291ac303f29cf8ce14e971e679820aeed54a18783c8a3/complexipy-5.5.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5edcae6572e03b2acac9cd6bb6b4be69725b8a8854ccca3ed6bb11420af183bb", size = 2474699, upload-time = "2026-05-22T19:39:10.973Z" }, - { url = "https://files.pythonhosted.org/packages/77/49/7e477d88b4ed4d98c3f6c38699aad1d3a267b2eb8d6337f0427292a3d5d9/complexipy-5.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4e9033a188508764223d1a586e57abdf14d9942e3359a34efecc66e4255fd24", size = 2378440, upload-time = "2026-05-22T19:39:12.876Z" }, - { url = "https://files.pythonhosted.org/packages/44/d9/d7fc53a2f8c4f2e362fbc4061460288c8778e1d27cc5eb19e47a08b6cff1/complexipy-5.5.0-cp311-cp311-win32.whl", hash = "sha256:c74311689c284b3cfbff691a4a8cfd508aafdc3fa79e7d9b9d07827fcfdde90a", size = 1915101, upload-time = "2026-05-22T19:39:14.387Z" }, - { url = "https://files.pythonhosted.org/packages/b7/60/9cf4e402ab6025c89facf0535ad6e3219f20c128ed251f8a708f693b5519/complexipy-5.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:a46b3e033eb46406bbbc174ce2caa69c0a3142d722b3556a83433218107afc66", size = 2041559, upload-time = "2026-05-22T19:39:15.877Z" }, - { url = "https://files.pythonhosted.org/packages/47/58/ecb3d760a1701877a07336c7f59e4304c9e9be5660143fd4d09428813527/complexipy-5.5.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:23bb9a77e62fb6616abbe42a4eaa8e24d286ea025f6831c8e047787c1f3d7daf", size = 2215455, upload-time = "2026-05-22T19:39:17.545Z" }, - { url = "https://files.pythonhosted.org/packages/21/0a/10215308e8c50b5263d5af2e3ef3ad16725b12d55c277f205f0a06a69db2/complexipy-5.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dc03f49d5450329c4edeccb22e4a0859c4236e588e9d07d639d88e2d7431830", size = 2136072, upload-time = "2026-05-22T19:39:19.169Z" }, - { url = "https://files.pythonhosted.org/packages/11/bd/b3fa9c1fe2a882e5b23043b4cec7dc626a37d0d5b722d5e330eff3c7bdf3/complexipy-5.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7c03e3dafcc8f1f99939c51ce490021ab63d554a95460f686e0e98f133ba47c5", size = 2318782, upload-time = "2026-05-22T19:39:21.232Z" }, - { url = "https://files.pythonhosted.org/packages/04/80/86be609cd41f886f0640227eab380f877d87a227a4ca96a44b7e5b4f2047/complexipy-5.5.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1c55cc3f3b414750a601113f3e1f6cdcf01131d5e65c2b2c46135d172824ae28", size = 2256143, upload-time = "2026-05-22T19:39:23.387Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d0/8e617ebc39c0b07cc8f96d6e79d760d1913ab5214cff0f025a2befaf83f1/complexipy-5.5.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:65bedfab6db826a23a3aa3b43fb4fc072c2d950e53678bd97f01122b3b0aeab3", size = 2456436, upload-time = "2026-05-22T19:39:24.957Z" }, - { url = "https://files.pythonhosted.org/packages/74/ca/a2c5d9c28badb5478ec89df3d0c038d1005f8982edc7f322903d10f1b5d0/complexipy-5.5.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:32890f9602e42047e92836f89db87e1fa8160e7ac216b0d3b4e406fa0eb7ae31", size = 2707617, upload-time = "2026-05-22T19:39:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/5b/81/83513e42306dd14dadce03610a3a04ec697aa4f5d64d4e42a39be121c8df/complexipy-5.5.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3099f55afb3c46f7f857b1380d276f68d6607cbb866ff59a652a3676a1ae953c", size = 2468505, upload-time = "2026-05-22T19:39:28.387Z" }, - { url = "https://files.pythonhosted.org/packages/9b/22/397796d26ef1629f73c5497fc56fed9935b080f292ad1c1bb76a58ffa806/complexipy-5.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62c8bfba0c618e5a94e2593712a7ce7a9c68a162114e9c84f95c9229c3a6ce76", size = 2375511, upload-time = "2026-05-22T19:39:30.339Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0a/f28a0ca289afc88a7635c82b63862bcb6205d00f795605b17d99fd585072/complexipy-5.5.0-cp312-cp312-win32.whl", hash = "sha256:c4d1512352d468afda78fb771967442d6c7b4fa31028a7cd6f21d2819146932a", size = 1918633, upload-time = "2026-05-22T19:39:31.916Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4e/8d1c3d02fadb6d96e0abe2bd444d5af38a383d4870b0567a7533c9de8c18/complexipy-5.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d7416dcfb63c96d76dc5bf43d1dd275eeaed3aa0ad372c95e71d2367dab0e97d", size = 2043839, upload-time = "2026-05-22T19:39:33.325Z" }, - { url = "https://files.pythonhosted.org/packages/93/56/359f9ae42bd3bce2b5727f9bd57c75978130dc15e56c10817472c869f158/complexipy-5.5.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:bccd39278271e59216aff1cb3b57a3e9d9a56aef1db9bcc151c10e5011fec730", size = 2215450, upload-time = "2026-05-22T19:39:34.859Z" }, - { url = "https://files.pythonhosted.org/packages/f4/24/bbf5ec15869714e7d2ef42f688b0f7782a69cf95ce43704f77ff632bc220/complexipy-5.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f56059ce198bd72fb72c0b92f57529e4ff245f804637dc535c50d0f3d4d0f39f", size = 2136071, upload-time = "2026-05-22T19:39:36.595Z" }, - { url = "https://files.pythonhosted.org/packages/a4/94/52f7581961a3e376d4410f27300bb9c9b2b9b42e1d42d8208ab29784c57f/complexipy-5.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3ccdbbeb00e25443ee13aa07dce052cd4021302d4b85b749f548f137b4fcc930", size = 2318783, upload-time = "2026-05-22T19:39:38.25Z" }, - { url = "https://files.pythonhosted.org/packages/ed/0d/4a5a84462dfbe487b6c27f8adb0184e4eb4755fdb0a4b7a1808f48df17aa/complexipy-5.5.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:55d4e2941005838a9dc56e9118d2bd5c3c2d748ce9b07f1ca7578c3a687a1314", size = 2256142, upload-time = "2026-05-22T19:39:39.929Z" }, - { url = "https://files.pythonhosted.org/packages/04/49/96e977ca0ca764530a6f5613f765e685d3f2b1f1ddd6e4e953271b6580a7/complexipy-5.5.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:784f330a52a309b45cd252a5e1e19cc1f47a31003c5785b2f260bed2be49926f", size = 2456434, upload-time = "2026-05-22T19:39:41.466Z" }, - { url = "https://files.pythonhosted.org/packages/4d/74/0c295cc9f2e4b4503cc720d7663eb420588c11a9feba7837083b450b1a27/complexipy-5.5.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:56dec39826804708ef8f0586f738495bb2ef1ff2aded1d77364ab9cc45a97de6", size = 2707618, upload-time = "2026-05-22T19:39:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/db/1e/793f89c4bc06a74fee3176c55808c4037a4465d986947bb5ca5745d2d8c9/complexipy-5.5.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a33f6b6d7a907ece905a0a8e40f56d34876af985cdc4eeed857ed311725e45d2", size = 2468505, upload-time = "2026-05-22T19:39:44.927Z" }, - { url = "https://files.pythonhosted.org/packages/af/7b/95debb9a3c3a263b346dbb19590c2088a7d78f7454dbad6f300c4af15802/complexipy-5.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19cf316636e98b6b5cab03e713c89ebf81b46ff01e002c2a8897fc629f78b594", size = 2375512, upload-time = "2026-05-22T19:39:47.148Z" }, - { url = "https://files.pythonhosted.org/packages/f5/81/1d07a97880cfc72b4ec66e06616a208579abc337d7a1ca532337546b27d7/complexipy-5.5.0-cp313-cp313-win32.whl", hash = "sha256:0cdca6fd5cbad53d215877198ed136f379c71b3ec6ba16ee0dd6c082d5111e58", size = 1918629, upload-time = "2026-05-22T19:39:48.739Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9c/52a7f72e941aa65ef3a48c95486293e0a015649631c56615ae9ba1c0ece9/complexipy-5.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:115cc4be3f13df41e2f7990837096bb84fb7e686c4db35c838dcd068a93dae88", size = 2043837, upload-time = "2026-05-22T19:39:50.514Z" }, - { url = "https://files.pythonhosted.org/packages/28/b9/5af652ca3c85eeb86707b945a29b38396263359eb4239a33d9f810e83bc6/complexipy-5.5.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:2fa1b99c947567150baee311779381d1844fecf57a1555bbed46f3a68f6286da", size = 2215451, upload-time = "2026-05-22T19:39:52.513Z" }, - { url = "https://files.pythonhosted.org/packages/0b/20/c32a46a0c40efb07b292902dae291de0358a90005badb29e859277cea392/complexipy-5.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c58f478c5c3971c5d80142a407b3d910eae4f133c3f4f8303adaf3dd022a874a", size = 2136069, upload-time = "2026-05-22T19:39:54.033Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f1/ddc0084447c58052aa6808d6fa29fb5200f6b40e3cca4b6537aeea1ceb7a/complexipy-5.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2a5c4b8572b4219e14631553c4c263e44dce292eed737c9bff0a4ed3f6004f3", size = 2318782, upload-time = "2026-05-22T19:39:55.543Z" }, - { url = "https://files.pythonhosted.org/packages/3f/c7/e44ae2dd6969c77acd942959b05c3a080bf7af6c72e0bbf08d2b70210d26/complexipy-5.5.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:154c16cb15c81f845f21c37daa5e261d531d4f6463bec890d61899b6be24ee7a", size = 2256141, upload-time = "2026-05-22T19:39:57.161Z" }, - { url = "https://files.pythonhosted.org/packages/76/9b/2d958617c57de367bbad32c3849be643363f8e9f764494fc993ba9a47f44/complexipy-5.5.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:de8c98e7f8ed44fa9bb327b58e6e5b6acfbf543e7ac07e6f19269393ca5fd794", size = 2456435, upload-time = "2026-05-22T19:39:58.882Z" }, - { url = "https://files.pythonhosted.org/packages/b6/cd/ace41a7f4f7f7c16863b14b342988190386e860bd06abc1801a01f43f8b2/complexipy-5.5.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0006c88baadc3d2e3279660b3e9d2c1dc629a6b40a4e7ae8a7cafe6b968d9cb9", size = 2707617, upload-time = "2026-05-22T19:40:00.747Z" }, - { url = "https://files.pythonhosted.org/packages/36/fc/e28417982de8b479261a7c8569dbc9645e6fdfe7c22e49e46d7347f5fd07/complexipy-5.5.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:22de21ec75276c6458192e7c291d162418a6c21071d8226e3447a4da72295cef", size = 2468507, upload-time = "2026-05-22T19:40:02.346Z" }, - { url = "https://files.pythonhosted.org/packages/68/63/76ea965e721de6ea055f7fd9684e4da7741240bf89b2f86d69774b3e64fa/complexipy-5.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:de60570bf5b968cc50d1554f970bc317893b67923910fac6b35aa38921e2287a", size = 2375512, upload-time = "2026-05-22T19:40:04.288Z" }, - { url = "https://files.pythonhosted.org/packages/38/28/af04683d10495824ef54555fb4ee666a3e7718c602215215a1857c304bb2/complexipy-5.5.0-cp314-cp314-win32.whl", hash = "sha256:c7714f9ef79479e399f5603e4d5c436ab088210762c46a283f20e6b6d6f4ed3e", size = 1918632, upload-time = "2026-05-22T19:40:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/ef/39/2d275b2660b06a6be9ef482d77c1f07fa2cbf091500b05beba55f349fc9f/complexipy-5.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:669702e9b4453cb721845058b1cced8e86f0cfa5139dc574fdfae0aa6b0be11c", size = 2043840, upload-time = "2026-05-22T19:40:07.41Z" }, - { url = "https://files.pythonhosted.org/packages/83/6f/3f4045ea3aafb0a3d66b5a41dcaeb9b5f4e0e2dc08f568e3b72deed0ee0e/complexipy-5.5.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6ce8be6261cb7e31a656c5961965b356f4d04a79830f54ce714abb76bae648a1", size = 2323801, upload-time = "2026-05-22T19:40:39.976Z" }, - { url = "https://files.pythonhosted.org/packages/af/ba/dbc5632e3df5c8a719fb21a489e6806b1e40541532cff696b52c9500b56a/complexipy-5.5.0-pp310-pypy310_pp73-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:6d6c1721416c4c9c90bed860e5345eaaa6c58e48c6230b7c448a170405d7eb67", size = 2255485, upload-time = "2026-05-22T19:40:41.749Z" }, - { url = "https://files.pythonhosted.org/packages/ca/64/613544650dc549d02d4ec227d05ef502c305801630d80f59107fd93c08e1/complexipy-5.5.0-pp310-pypy310_pp73-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c5967735775e8891ece158ae95b441facb6318ccc3d716c173b13a3368339a8f", size = 2710352, upload-time = "2026-05-22T19:40:43.481Z" }, - { url = "https://files.pythonhosted.org/packages/15/14/d32a88bf29c28550979b7efd71a42c396350febc64cf39da647c16c98bb6/complexipy-5.5.0-pp310-pypy310_pp73-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:09e7cda289cc2c15d3adaca37f31bbf7fa55c4cbb9456f8d1f6e47f215425ef0", size = 2473941, upload-time = "2026-05-22T19:40:45.014Z" }, - { url = "https://files.pythonhosted.org/packages/db/fe/6426d1cb284d59345bd23ece4b33f208dca157a50f5a286513ef8e125483/complexipy-5.5.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f27ac42332e27c9832673edd25451d4614e702dd1f405e0d3a65dfc50bda2cbe", size = 2323932, upload-time = "2026-05-22T19:40:46.663Z" }, - { url = "https://files.pythonhosted.org/packages/92/19/06c0149de4d711e1e53358be12949b814942e334ed59e9364e062b68ffa6/complexipy-5.5.0-pp311-pypy311_pp73-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:868187d7d13915ddce824d7df1ef871351674d76fd808136d7f19a1408ca9627", size = 2255572, upload-time = "2026-05-22T19:40:48.832Z" }, - { url = "https://files.pythonhosted.org/packages/31/90/eb59d4f58b834ca4eff567b86ae5986020bef2609406b0c69e5d4eec437e/complexipy-5.5.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:061ba79c6965069b30d955574f8ab86a75069fa1cbf1c4bf61113076a10994d6", size = 2456784, upload-time = "2026-05-22T19:40:50.34Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a9/0c78961f4441ee7bcf3cb7e653796eae74c374cc60d6f7d09d4bf05f95f3/complexipy-5.5.0-pp311-pypy311_pp73-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:77e8a19c40d64b9ea90b53bcf8eeeab5990d28bf0027b9cc4e60d13b8982b28c", size = 2710393, upload-time = "2026-05-22T19:40:52.541Z" }, - { url = "https://files.pythonhosted.org/packages/85/38/fca125250bfd7b9d433e91fc39e92563185a510ab61be2457a19b2d64639/complexipy-5.5.0-pp311-pypy311_pp73-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cd76fee9efa2ab6df802b6114495a943f33b4d0e3c61e5d5fbcefdb1d9d6c847", size = 2474038, upload-time = "2026-05-22T19:40:54.19Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f1/a7257d752d61657c33e47a792770f222bad88c65f58881dd2ab387e15ec9/complexipy-5.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11612a23d3fd3eef408c70dac5717d01cc356886bd09aaf95873403d5a6997a7", size = 2377453, upload-time = "2026-05-22T19:40:55.699Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/b3/5b/8ad2d572e42e222d8127b37cad8595d6b7bb1d1d8e0a5bd3cb378f0f1d2c/complexipy-6.0.1.tar.gz", hash = "sha256:ab0ab0c38962a3f58062a16badfb30b88c4a68029040fc5333e1d644b41dc744", size = 352346, upload-time = "2026-07-03T03:25:52.548Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/6c/a903b942a7fbab8c468f383d7b0afd15c8d7e52105d05fa95d63218ceb8e/complexipy-6.0.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:50258eea7f88da2b3ead49efad2ccf5d3c3a254b3dc6645c3d94c71bbb1055a7", size = 2333876, upload-time = "2026-07-03T03:22:51.355Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/23c2afc31beee7a52d3b2ceaf908614e2a555a7fc97e3a2f2ad0d6dfa7bb/complexipy-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:320e313c519a775871e9e9862fcbb9ad3ed01dca4a990658c13df9dbcefe88f0", size = 2276454, upload-time = "2026-07-03T03:22:53.209Z" }, + { url = "https://files.pythonhosted.org/packages/74/8f/91dce12a3939ebcce30145eff4cbed4c6cc15fc8f1f21d1a38d7f52ce855/complexipy-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e7a8f27c286ae32e51291ce84550b18566c03fae0f53f31feb14f2182a7571", size = 2458934, upload-time = "2026-07-03T03:22:55.087Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fc/3b367e7c0a28ede6b69b9bbba84c8149bea999b92973cffc070071547ffb/complexipy-6.0.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90ee49e83910382937f5b847193e5de7b2f6da40313d2ad56bf49e9fa412b665", size = 2384361, upload-time = "2026-07-03T03:22:56.611Z" }, + { url = "https://files.pythonhosted.org/packages/28/ce/33b5a3d08ccf567b33287fb54e50fe962c8ddbc632eb81833f47064bdbab/complexipy-6.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97d0cbb37114a5c6f646eb09c6d19f2ef8724876df6b9a6fe1654e02e37a7ff", size = 2605630, upload-time = "2026-07-03T03:22:58.322Z" }, + { url = "https://files.pythonhosted.org/packages/21/9a/7bba38697fc37ba872b81d9a0eb62113e398dad0e92fc8cadbf483d83522/complexipy-6.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90617f1738799d109294bd6e765e16789931b77dc8d24f9431c8d4f861f11566", size = 2843859, upload-time = "2026-07-03T03:23:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/1d/16/c8091938edefa2f10deddb10ad3fee5434d6564f6ba023cd09c8af9ca656/complexipy-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c240b74d3d3a02bb9e6b8872fccf06db0a7fef7066be5693d261cf5a164efb41", size = 2517169, upload-time = "2026-07-03T03:23:01.549Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/32c4be77158396928dd260363dbfce1f09e0a5bc5da15fd43ca356326af1/complexipy-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bcf4f2587567531133856bd2798a9f0ff071b3f74966512da6f323dac533858", size = 2499806, upload-time = "2026-07-03T03:23:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/9f/08/94ebfd1c21987532e6a346ddd9708761e44b820d15859184208f6bbc7ceb/complexipy-6.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:07f8f2920c5b9f692c69b19da8e5a53970c2a248ee0ef40149be7e8c8381c499", size = 2635133, upload-time = "2026-07-03T03:23:05.005Z" }, + { url = "https://files.pythonhosted.org/packages/3d/75/9adf51d2a1bd1e118bd1946effbe4286d007362b9c91864cbfc4b88a9bd7/complexipy-6.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:80f3bf76fa9ffe7a8b0dde2d18bef377ef1a27d31f91f8c7c751fbaf0b71cafc", size = 2731679, upload-time = "2026-07-03T03:23:06.577Z" }, + { url = "https://files.pythonhosted.org/packages/1e/45/eb5379016d2b16ed1f1561a2d0161e8133736bdab8951037da1daae42d52/complexipy-6.0.1-cp310-cp310-win32.whl", hash = "sha256:47534accd56142835ebde5cd5250b76cff59fb1d41a98985735622cfb66c80e4", size = 2034331, upload-time = "2026-07-03T03:23:08.233Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e9/39ab66dac4a0086888f43030c6398d377e48d94e8b85d1a616f8f6d2c6df/complexipy-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:bca7a707f58df55ae38696acb6a97ccfb4d09eea9c9ef920183bdc5f83dbe347", size = 2189726, upload-time = "2026-07-03T03:23:10.012Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8e/0e4fa7911073db04a1cc3bf1e9acccdc66a212750f4c8a07ecc4ae15d62b/complexipy-6.0.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:db4485c45f2d58e8c8e0271a558df456e30b5845215c8931083d7bdd4ca1db46", size = 2333668, upload-time = "2026-07-03T03:23:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/be/eb/26d3bf2ac0d523a348b002a45029d329c8cc23171148c3026247fc4e9ad7/complexipy-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9d443e4d13d7e602484f4e72d60fd7a35fba4f7dbf48ae31d25789ec2d112878", size = 2276554, upload-time = "2026-07-03T03:23:13.201Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/4481ca0dcd37ffeccf85f4f6754ee6e36b6e5439b09828f9de514b478e8a/complexipy-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f75836f1b1ff50440ed11fc66e0831382fe9084a0f9ed7c526b87345246a69c", size = 2458668, upload-time = "2026-07-03T03:23:14.769Z" }, + { url = "https://files.pythonhosted.org/packages/09/a0/3229dd4d9b186dbe389e350a3879ec7a761059580ebf64b25ccf891e45c3/complexipy-6.0.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:564ca9ed04ff6607d213c225aac3e887dd8da813a4455d80e88817f08293938f", size = 2384505, upload-time = "2026-07-03T03:23:16.347Z" }, + { url = "https://files.pythonhosted.org/packages/6d/60/bdc6f23b0d6271553e4a7c6a38632f70fb5a10332e05d70846215264a3e1/complexipy-6.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb5c5859e2c2eb602973bacd1615efba498345642a0f27d5535b790b7a538dbf", size = 2604558, upload-time = "2026-07-03T03:23:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/fa/87/3f59b76b918067f37f275c2e633a7330c000f97ab911f1eae82245bfcfe5/complexipy-6.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c207d4aab6ac6d956dfa75fb17a9398942dcdaf4f340c6865eb2f40340a7193", size = 2844187, upload-time = "2026-07-03T03:23:19.454Z" }, + { url = "https://files.pythonhosted.org/packages/c0/85/ab0fd546b63076ea51a4cdb6eb39a0b572bd06d60a9be0b04bf2fc266ca8/complexipy-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1415508e7b7f1a44f5fdc30a827d725ee591985b374039409b18f405bd075746", size = 2516347, upload-time = "2026-07-03T03:23:21.146Z" }, + { url = "https://files.pythonhosted.org/packages/db/ac/96b45d993895e3f1ebd8d4b9275e4a9b33d8da251c58b78fa74fd519707b/complexipy-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94905dc2f21eef657fdca8478126ccc995f2d0492539efef580a5a89837aa8fd", size = 2499771, upload-time = "2026-07-03T03:23:23.137Z" }, + { url = "https://files.pythonhosted.org/packages/78/17/59f40d19853cfa1d10eb743969b8e759ef02a7feb27289c155a5e7ad4132/complexipy-6.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cbbb6d8a7f31e9f4fc0e83fdc9c02dec87cf4f1dcdd1b639ede75d9fbc3e5c51", size = 2634540, upload-time = "2026-07-03T03:23:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/0f/46/9ffabed97ccd80fce83247c52a7c5e7962a79b6016a9d354ef76f756fef3/complexipy-6.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:412fc1c2021a762360752dcbccad88dfafa3d709e8961ca504114797db1aec90", size = 2731161, upload-time = "2026-07-03T03:23:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2f/4f85b091e968f9c370555c01455bda6ae858e17c609299477427b6653404/complexipy-6.0.1-cp311-cp311-win32.whl", hash = "sha256:4581ccc28103732d0fb158f0da320c33391f846b7592572cb2c9706dde5107bf", size = 2034462, upload-time = "2026-07-03T03:23:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/f5/0d/88ff8980fd1f716f096feaa54d7c4d2cf98e4a631f1a98abb0ed599c9b89/complexipy-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:7bead6b127f666c17d4e616bc26581728258c0e16b9870db0538c5945cf97fd8", size = 2190037, upload-time = "2026-07-03T03:23:30.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a8/ee7a6bfe3d54a62f604918b2de7e66f342a33414aa38d0584531762b3286/complexipy-6.0.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a00a018244c5e6f0b6e145517ff59f40c8455775cf0be1337e47d8ba8fc08478", size = 2333561, upload-time = "2026-07-03T03:23:31.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/42/07aba6c0fcf8db82027d3f915d121c52cdf6927722489bc2c8f81ee31f5b/complexipy-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a4a6158431bbccfc5d033c46cafe683b204090a7d62e2d708fc3e7ff0009c09", size = 2270718, upload-time = "2026-07-03T03:23:33.5Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1e/5b869db5eb6213623b9d6f8e0a97c7f53bec0963d8ea2a66ac4ce63b772b/complexipy-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc2fd8ed07086b9f1d669da6f901733984721800854807ecead2bdc79414bfac", size = 2452189, upload-time = "2026-07-03T03:23:35.067Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a7/a6c2c0d28da6caa292a44ffe6bb99bc8217c5222511fdb773de60c6e70e8/complexipy-6.0.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02d4045421fc5f1fb5cfb04ba3a7dac705be76fd45e80da5c699a98735d0b3fe", size = 2381579, upload-time = "2026-07-03T03:23:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/fca14853331bd5ce966db33bbda982a6c0b640d14f53c0281e1c81f72cdd/complexipy-6.0.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7a4b4058b19b832a43ca4bf370ce8c618489fa3bc5354c247a4d9a9f7593e37", size = 2598711, upload-time = "2026-07-03T03:23:38.862Z" }, + { url = "https://files.pythonhosted.org/packages/b3/01/1686158d43bd8397171a3c2f0e7a7938b861fc822c6346b02bb72e305dd8/complexipy-6.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbfa2957ea052d3749bed14b0ef78b93c8b443b2fd558a1cb585f574d2f68e3c", size = 2837911, upload-time = "2026-07-03T03:23:40.491Z" }, + { url = "https://files.pythonhosted.org/packages/a3/8c/19f4a3ca2cf4245fefb681a872ebca4a52ab2ebb1d94893da97c1bad684d/complexipy-6.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7df5faeb40b1d3b9020fb42acb29370571661715d3cb853f98aa5d6a242d545a", size = 2514337, upload-time = "2026-07-03T03:23:42.044Z" }, + { url = "https://files.pythonhosted.org/packages/62/39/81423ffe22709d8f4c3918f03cbf9e3d7e0dffaf73cc1c167769bc08a06e/complexipy-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3aa9b1efec9dd32714a40af3ff545bd6024def2dcef37940c91643fc35486f4", size = 2496146, upload-time = "2026-07-03T03:23:43.68Z" }, + { url = "https://files.pythonhosted.org/packages/59/c8/438b45da13ed43a44135da34abdb9e48ba8d3332ee1166bd58c59cd03675/complexipy-6.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15bdef5eff10cd801f5fa44243b5e110bc4f44d194f62b80c196245e798a896c", size = 2628582, upload-time = "2026-07-03T03:23:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/36/47/f5fa4218d0acd80f1a17fb4dca64c8c7a278bcef40755c8340a68e546e9c/complexipy-6.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f7e89d0f286c3d327e257b2e874c646e9726f9e09caa59b903a9f9f15b46d962", size = 2728422, upload-time = "2026-07-03T03:23:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/69/dc/94feddb3cc37c6bdcc445b37e7ee1e99151644511a07bdd03c98fc8103d5/complexipy-6.0.1-cp312-cp312-win32.whl", hash = "sha256:3513b6b6afe73f71ca8c1ff52c5b1a8d1aaebb9934d2279bd002e9fc4b6fbf8b", size = 2031415, upload-time = "2026-07-03T03:23:49.124Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8e/ea9b294b9fca952076a1edd826f6ff964913f36b28654f757587a7207273/complexipy-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0ba9cf20affaa91979cc9a664d833f29fc0c02c44b91a889930c434e4c3e8f65", size = 2185216, upload-time = "2026-07-03T03:23:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/97/b5/8cbadf95c24c08c784ecb6c965d6fda10f332cddf7ba6a5221af1c835a33/complexipy-6.0.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b51ef6cc39fb678d417a2145a65596ad5904a4e87137089e32d409ec38e13eff", size = 2332682, upload-time = "2026-07-03T03:23:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a4/78dda753999d5706a05489ed37fc11f1140fce93ea6db993a4ec738bd7ed/complexipy-6.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7b2c57c166fc72d8f5fcd2f84b2577201ec3546b2c95c2a637b074d2e462c14", size = 2270641, upload-time = "2026-07-03T03:23:54.157Z" }, + { url = "https://files.pythonhosted.org/packages/32/b2/3621279ba8ffe008dd7d6ab77132a4a9d9c7f5558eecbdb9c06f689a1cb4/complexipy-6.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01970de229707df12d78e2b0dcd4227e863d8224790ed0cb43a5501821736a6a", size = 2451874, upload-time = "2026-07-03T03:23:55.698Z" }, + { url = "https://files.pythonhosted.org/packages/91/d0/ca2b5d3d0e7ea098c8171ca4c14e791a5d47e3cca2fa1067b4bce83c0610/complexipy-6.0.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f300897b20ab75c8bb5c26aafc69a6feff022355be0af59d8c8314b1d408a4e3", size = 2381901, upload-time = "2026-07-03T03:23:57.512Z" }, + { url = "https://files.pythonhosted.org/packages/a4/72/b8d11386924ee291f86d60a58bc5e9cae50c739a569b9abab3be23d6e477/complexipy-6.0.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b65d9673d15e372629f9b564fbb4ce46f9293cba721c9be3def490aa8b1eb7", size = 2597418, upload-time = "2026-07-03T03:23:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/f0/60/bec4001c8f384d636aa8489309278d59e1699c8864bef321703238f2d872/complexipy-6.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c51a3ee518011a554624ded5bb8665eac3c9080636c3cc08efd7282eb718b2", size = 2837794, upload-time = "2026-07-03T03:24:00.953Z" }, + { url = "https://files.pythonhosted.org/packages/d3/33/158945d9fec78d854c15147b2cf1ecd8ea6f6410ff75ca2aa0d8e79f2784/complexipy-6.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71549e2b14a3fbdc73c3d87f2b8afea86a196d727ac0d9b07c0c3edf92b6afef", size = 2514715, upload-time = "2026-07-03T03:24:02.546Z" }, + { url = "https://files.pythonhosted.org/packages/e2/55/10689195791286c69a6dc5d15e75675d4dd5aa3682f330b7239bbd63c182/complexipy-6.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4199549e08ba3e8d40877da74225efcad39caf7eb35f07ff3cc4ba8a6bbcc00", size = 2496272, upload-time = "2026-07-03T03:24:04.178Z" }, + { url = "https://files.pythonhosted.org/packages/57/3a/9bd04f1985686f926ee146a78749e08363ba2a7c6d101d49d4a358f719c0/complexipy-6.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9ad6516ede6ba71d8b68cee1b8f670d6ac6d9de88cebd82951d2f893587f4ae5", size = 2628251, upload-time = "2026-07-03T03:24:05.998Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e4/b338966c3106344e49cd9abe650840f67969a8abf5d6445e185e1019bc84/complexipy-6.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d86e41ca6264778344fc7bf9ad101de740d94ec0e51bc1c7d5eb06b29d62f56", size = 2727617, upload-time = "2026-07-03T03:24:07.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/a4/f4b756595968ddf76c70219a253271eaab83ddedc7000296b7410dbb090b/complexipy-6.0.1-cp313-cp313-win32.whl", hash = "sha256:3ad75ba1918af0b88dee5aa5e35c99d3aae705e0fb55b9c518dffbd9bbf5cab1", size = 2031258, upload-time = "2026-07-03T03:24:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ed/a4a55b9cefec4f8e5b0630614e0fdd6f854d96ad2afe5fd6a5c67cad10da/complexipy-6.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:914da63e59780fd28f29ad84d51473ddfb4d35cfa600d9231eb92f1ccc3c83d5", size = 2184770, upload-time = "2026-07-03T03:24:10.991Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/819f85bc6598b3228461e3e0870300fe93b989ad02d59a25a7e647af0203/complexipy-6.0.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9cdd4b30b7f2151a16f5827dae20ac4be8544c13ced4a9fdb4daf37bd38c7b82", size = 2334443, upload-time = "2026-07-03T03:24:12.733Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/84ebf2c02f65d9e518a364f58779c46e118ca8b2ad8a39f342cee932811b/complexipy-6.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dd7656fe1c3e6bd5ea2ad3e042e2de4e26c2fb60e6f8ec1871ed75d9d1adef7d", size = 2271854, upload-time = "2026-07-03T03:24:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/43/34/ad2b796d21b05f0b9ae2507c9c4cf57a3abbe4c4806c856f5f12e1bda274/complexipy-6.0.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c790634d70faaaa90f01124e305ed0db6f30668e260fb578d7db3c35a106e0", size = 2451616, upload-time = "2026-07-03T03:24:17.044Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d6/da63d7b1557546c14294a47ee7617838b5d35508738c5cf65b37b38e0df1/complexipy-6.0.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbad719a6a74db3cb952abed96c5ea172ffb78416a143271e2736c3660b3e354", size = 2381811, upload-time = "2026-07-03T03:24:18.684Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d7/cf08b7155f38fda311aef87b67ed6f47bb74f56f878bf1af7635e074f6db/complexipy-6.0.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf32d9bde0fe90b7c0c36c56fcb8adf7810d4b0eda0ee1d37019ff31ced33c3a", size = 2598714, upload-time = "2026-07-03T03:24:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/8d/57/c363a9a6b3efd98b775fb9bf138dfdc5cc035edc15f501f2865f189aa0cd/complexipy-6.0.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf4602eb9896cd21145fd6dbe40c101dfd45b9d55a316e93dca256b2939738cc", size = 2840964, upload-time = "2026-07-03T03:24:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/50d536bf67323e0abe2ae5ac929750402ea48dc0275ef826e604230a4356/complexipy-6.0.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d4903b4cf14bce2e5b51e417c1875cd64b72d03c1886dd959da00ce69772fee", size = 2514328, upload-time = "2026-07-03T03:24:24.616Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/635c0291acb531cb15ce138d976c8ecc021c9d8c84286da4efa66e4f68d1/complexipy-6.0.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7fa5e803bcece8e95f98570e03a11455c889ab780c6b43108aee0c48340191", size = 2496921, upload-time = "2026-07-03T03:24:26.292Z" }, + { url = "https://files.pythonhosted.org/packages/5b/39/fb86363098388a4c93bedbe4fa1e1ccb250e8f3d3ca3dd5aae0b1e147ed4/complexipy-6.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fac0f09da5c831ead52c060c49b0cfdfa9f59b9f7d76648f00d8aa320cd598da", size = 2626855, upload-time = "2026-07-03T03:24:27.964Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/2f23cf237eb8b948494e1cf323c3d8b25ff570aede04d0ddbb4465dda604/complexipy-6.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4cee67c6fb1e98bbc30ba44dceff10f64822449d7ee46d71aaa4239f381e6825", size = 2728611, upload-time = "2026-07-03T03:24:29.783Z" }, + { url = "https://files.pythonhosted.org/packages/06/34/e5bcd00f0581430f7ff10a5a0b21e7bee5ee385a2bc6c19fdbf1404075a7/complexipy-6.0.1-cp314-cp314-win32.whl", hash = "sha256:f50d2b62383715f88e6e9a28705ce341e6c8b4d80ec699a823c6b079a8c7ca03", size = 2031432, upload-time = "2026-07-03T03:24:31.815Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/d474bcc4de54bc972088069c66a20be78ea0bd080d2b0b971897dfd77a3b/complexipy-6.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:d5e31cc445a9fc787a47b438f495069a303072b03608e649900b4c0865f4f6c4", size = 2185680, upload-time = "2026-07-03T03:24:33.587Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f7/a00bad292745623d1b16fcdc3f5b933fda31b1f69ff8c2bdc5c3922cf996/complexipy-6.0.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635bc9fe5aa26ab07599872ce529da7770af1870417712f8954e0ca7e08a2de5", size = 2451689, upload-time = "2026-07-03T03:24:35.189Z" }, + { url = "https://files.pythonhosted.org/packages/eb/70/5021c3f8dd6645e00ff8b6f9da549ab3f1d75dd036b1517acb790cf2b261/complexipy-6.0.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92697f7536418244c4d0286c0cf4aa76c86b37835aea438a530956e5fc53c7ec", size = 2378153, upload-time = "2026-07-03T03:24:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/cd42a9a4d7b6c488fa1bf1f0f030da87931fb770a3ea0aef13d5cd614839/complexipy-6.0.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a69df4a46b79dce380979e07d4c6e87d0aaf7943669c20672635d45c612e7bf", size = 2597988, upload-time = "2026-07-03T03:24:39.429Z" }, + { url = "https://files.pythonhosted.org/packages/53/d6/ea382f6f43f79a25264813de5ba90f20edd654063403123fe5b8013afd1c/complexipy-6.0.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be35712c7d5ebdcc91fcfc8c986540f40c22c7fe08d8233df5406e91a08b5918", size = 2836822, upload-time = "2026-07-03T03:24:41.209Z" }, + { url = "https://files.pythonhosted.org/packages/56/3c/44d0191e59f6f65477b5c7f8940cecc498574b2f34067fbc83be08452df5/complexipy-6.0.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87a19d4baac23883198867e48f3f84af8bb5d87b3e0a6d67e34e328576a7aafe", size = 2513728, upload-time = "2026-07-03T03:24:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/06/f3/58e50e2b4031be7b0ca09a44d013d7b367888a4d5e74fc16daa3b09b49fb/complexipy-6.0.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a15c2167caa2d187c060869dc20b9cacb2bcfccd557cced4326b4696ad2941a", size = 2496613, upload-time = "2026-07-03T03:24:45.782Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a0/1aad33f322c7f25f0bfa168c19ee0df9e95423c01827c7f4f9b89666c9bf/complexipy-6.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9232716d9314f12165b32b5ef533af0526b2ee64fd768dd7c816e3bc020a385", size = 2628105, upload-time = "2026-07-03T03:24:47.59Z" }, + { url = "https://files.pythonhosted.org/packages/df/27/4d1ad63f222d218f8513cd5bf88baf2388a4cdc00488d0b4f22e461fe57e/complexipy-6.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3c57db8b2b4150cf5ee6738d2048f4cee0d674f18ff1d124f063cdba6ae4ac67", size = 2727809, upload-time = "2026-07-03T03:24:49.844Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d0/746d70761b0d8b06ecd0ef8e1343a173c15c09135142ffe26f528063dfee/complexipy-6.0.1-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e49e032cc28d194f44cff1e9eb6660385007eebdfde9e5b748a7a6c70878dbb", size = 2599630, upload-time = "2026-07-03T03:24:51.71Z" }, + { url = "https://files.pythonhosted.org/packages/5f/65/629a909a351d4690a52e435630d527f143b319b181eb2cce20c47fa6e0e2/complexipy-6.0.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78ca8529285881e2deddd72e245e4ff1daa3a9e72eec0371112e4146364dfbd8", size = 2497198, upload-time = "2026-07-03T03:24:53.498Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/4ff4defe8517d33db6eecdb55d418678dac0f3b03e805ce3a2565e2323a5/complexipy-6.0.1-cp315-cp315t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb94e4d203eb2a3a5c5b9588d911450b76d817e04bf81900d225a72a53a17b48", size = 2598463, upload-time = "2026-07-03T03:24:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/da/b1/695c48cbb030235e1bf7b93e98ae37535da533a7de49fd81a5230673e3bc/complexipy-6.0.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:393aa190df961a17cf7ef582b652a07c4b8402cc00a40f06098bf119eaa764b0", size = 2496649, upload-time = "2026-07-03T03:24:56.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/84294c68e5ccaf518ee754912193635fba03b95842029ce3f57dff739936/complexipy-6.0.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13b8982ea24ad7dc67d19b98c69830ca64da835453a587444b94fd47c5bb1adf", size = 2460225, upload-time = "2026-07-03T03:25:37.991Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a6/9583d15fb284c57ac80c73ee93fa6ddc6aee190ec0534b34ba417da723f2/complexipy-6.0.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55029935fa6df27f612f1711a06fae54cb199b9c3446ef570dd96db1271892a5", size = 2385434, upload-time = "2026-07-03T03:25:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/6753d99a3e84198cc6f3f6147e81a53fdf22b35d37e22f5f9c42b6e6f91b/complexipy-6.0.1-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73356775f48e29ce69d7a4f60000e12c2287afd5d99563f070f9a53f0557318d", size = 2606627, upload-time = "2026-07-03T03:25:42.215Z" }, + { url = "https://files.pythonhosted.org/packages/92/f1/9bf5d43760ab3d3cdbb58feeb2947c7d276a867cc295001aa2c8b76d9e8b/complexipy-6.0.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9583ec5923fe955e82db81f64c2cbd7a01e68b3ea0b09f198d31dd53cf78ded4", size = 2844100, upload-time = "2026-07-03T03:25:44.087Z" }, + { url = "https://files.pythonhosted.org/packages/17/22/63a8facec56d648ec0ae7c50409e376192dd58a9e818286ea73c2a82b3b3/complexipy-6.0.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc5619f415511beaed28ee1c8066b48eb599a9a7e1b64e308ded88f9bfecc928", size = 2518321, upload-time = "2026-07-03T03:25:45.743Z" }, + { url = "https://files.pythonhosted.org/packages/70/5e/2b19ee2ee2a0d0ef863ea8f221d0c79a2ffb971c110e0ab5fe7a19502847/complexipy-6.0.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b0303c21800eb5d6bebcc13c10db30ac1b63387df727c9a51bf2f9ef0715ef", size = 2499692, upload-time = "2026-07-03T03:25:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/8ba43f44d19ba77021718e1d0ae068899de7d8262d6c3f5dd907c35a2341/complexipy-6.0.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d972b20df55e777a704d8ec2715b3a455b1912a3b81e993ee1d22e498e5b83f2", size = 2636314, upload-time = "2026-07-03T03:25:49.187Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/73f3bd64c8968bcf672f0a2feada1f495a041cfcd61a5f777ff681560e3e/complexipy-6.0.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:523245310a7527ce2a01b03f1deb8fa058cb46dae5d94285df4ad6369d4d5695", size = 2731722, upload-time = "2026-07-03T03:25:50.838Z" }, ] [[package]] @@ -589,115 +609,100 @@ wheels = [ [[package]] name = "coverage" -version = "7.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/69/0d2ef01ff4b8fcecd4cba920d11e92fa4f96ae412441d3b56a90a258e69b/coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf", size = 219722, upload-time = "2026-05-26T20:38:14.002Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ae/9afdeaa31b9d9ce98124b6abf8bb49119bf71aecae04f8567c189d91299f/coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf", size = 220240, upload-time = "2026-05-26T20:38:17.424Z" }, - { url = "https://files.pythonhosted.org/packages/51/69/c998589871df7ea7dba865cc5ee32b5a3e1d47ba6c68ef91104c7c46fa5e/coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d", size = 246981, upload-time = "2026-05-26T20:38:19.266Z" }, - { url = "https://files.pythonhosted.org/packages/fc/10/1c7d04c13040dac531d21b712bbe08f902e6dd9b58f5d77875c4d030f8f2/coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2", size = 248812, upload-time = "2026-05-26T20:38:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/c1/65/2a38a4607ef27cadcfbcee034dba5830ae2569f90144a0f4c7dbf47d30b0/coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47", size = 250675, upload-time = "2026-05-26T20:38:22.159Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a2/a446ed9752a4a59b79e0fb6cbb319f6facb2183045c0725462625e66f87e/coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550", size = 252590, upload-time = "2026-05-26T20:38:23.63Z" }, - { url = "https://files.pythonhosted.org/packages/9e/fd/e81fbd7ba752365546e9842b1cbdaad3d6919d2a522c590aef16a281ec5e/coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e", size = 247691, upload-time = "2026-05-26T20:38:25.057Z" }, - { url = "https://files.pythonhosted.org/packages/53/35/f3c26fdaae9ea937d154ca4d372e5ea0a4167ff70d36c6074ac2eacb2f83/coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f", size = 248716, upload-time = "2026-05-26T20:38:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/2e/14/940b6c49551fd343e8507ee2b0ba7af5d0aa04ed5bf768285cb7c72a9884/coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1", size = 246721, upload-time = "2026-05-26T20:38:28.282Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2c/40fc0634186c28292a662dff578866b3913983d6c375a3c2a74020938719/coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5", size = 250533, upload-time = "2026-05-26T20:38:29.753Z" }, - { url = "https://files.pythonhosted.org/packages/de/e3/2c26bf1e811f9df991ff2a9bdddebdd13ee0665d564df7d05979f9146297/coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b", size = 246990, upload-time = "2026-05-26T20:38:31.516Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b0/060260ef56bd92363ebdce0c7095ce422b06e69aae71828efeca473ab1ca/coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332", size = 247593, upload-time = "2026-05-26T20:38:33.065Z" }, - { url = "https://files.pythonhosted.org/packages/63/f3/501502046efeb0d6d94b5ca54941d95f1184183dd6bdb7f283985783bb4a/coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59", size = 222330, upload-time = "2026-05-26T20:38:35.36Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5d/1bf99f2c558f128faf7906817ccbdb576ba815d3b41ce2ac1719b70a3663/coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253", size = 223261, upload-time = "2026-05-26T20:38:37.196Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d7/477ad149490e6cb849f28abea1dabb9c823cea72e7500c81b4240ce619c0/coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f", size = 219848, upload-time = "2026-05-26T20:38:38.715Z" }, - { url = "https://files.pythonhosted.org/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4", size = 220354, upload-time = "2026-05-26T20:38:40.232Z" }, - { url = "https://files.pythonhosted.org/packages/43/8b/78419b5391a5cb706b6544390507e469d83ffc9a8248b02c4011aceb9365/coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1", size = 250771, upload-time = "2026-05-26T20:38:41.782Z" }, - { url = "https://files.pythonhosted.org/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f", size = 252683, upload-time = "2026-05-26T20:38:43.305Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129", size = 254791, upload-time = "2026-05-26T20:38:45.361Z" }, - { url = "https://files.pythonhosted.org/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860", size = 256748, upload-time = "2026-05-26T20:38:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c", size = 250907, upload-time = "2026-05-26T20:38:48.493Z" }, - { url = "https://files.pythonhosted.org/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7", size = 252483, upload-time = "2026-05-26T20:38:50.074Z" }, - { url = "https://files.pythonhosted.org/packages/43/61/331c74103c62dcb0c4b9b3a0de9a61aca016208b0a90f109592a9f9ecc28/coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec", size = 250545, upload-time = "2026-05-26T20:38:51.613Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef", size = 254310, upload-time = "2026-05-26T20:38:53.503Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df", size = 250266, upload-time = "2026-05-26T20:38:55.414Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9", size = 251174, upload-time = "2026-05-26T20:38:57.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/10/e55307b622b3dd9671cb321824502dc10f93e72f2802b9946159a8edadeb/coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548", size = 222354, upload-time = "2026-05-26T20:38:58.727Z" }, - { url = "https://files.pythonhosted.org/packages/71/cf/107421693cfb71e4f1ca5bf70443f64d4161878068d07a3e51c7ad21d17b/coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e", size = 223290, upload-time = "2026-05-26T20:39:00.413Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1d/3e3644585eb29e9dafefb19555078529a4d7cce12bd21929664eea989277/coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3", size = 221953, upload-time = "2026-05-26T20:39:02.159Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" }, - { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" }, - { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" }, - { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" }, - { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" }, - { url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" }, - { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" }, - { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" }, - { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" }, - { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" }, - { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" }, - { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" }, - { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" }, - { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" }, - { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" }, - { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" }, - { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" }, - { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" }, - { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" }, - { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" }, - { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" }, - { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" }, - { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" }, - { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" }, - { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" }, - { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" }, - { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" }, - { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" }, - { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" }, - { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" }, - { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" }, - { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" }, - { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" }, - { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" }, - { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" }, - { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" }, - { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" }, - { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" }, - { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" }, - { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" }, - { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" }, - { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" }, - { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" }, - { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, +version = "7.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/97/c52dc440c390b6cfa87be9432b141a956e2d56d9b9f5fc8bd71c5f471722/coverage-7.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50913d4bf5ddafa6ca3693da5e4dd833dd1b772e0283c99ca7f7d287db67331a", size = 220539, upload-time = "2026-07-02T13:08:19.252Z" }, + { url = "https://files.pythonhosted.org/packages/3f/26/602de8c2aec7e2e3e99ebfb8e04ba65598f746275396eea5f6794ff4673f/coverage-7.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:359e141ccd33893ce3f1ad5525f8b96083003677c82182e5907d62d4ea5799fc", size = 221058, upload-time = "2026-07-02T13:08:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/fc/13/ebab0743138891c1d646d61e247ec29639afcbb6c4e1905e6a0f0c75291a/coverage-7.15.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3200b6204935f928c64b2ca1f923ab8c1acb7c9de45ec61569711b34d25cccaf", size = 247797, upload-time = "2026-07-02T13:08:22.474Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/b6ffb9e042aa48dc4144a8a65529affaec8dca0685309353614a2a7386ad/coverage-7.15.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be616bf61346883b2cfdc5178669647e03531d81ab761a7e378558b7e8bcb628", size = 249626, upload-time = "2026-07-02T13:08:23.803Z" }, + { url = "https://files.pythonhosted.org/packages/9c/06/243ff05b652333d8e3d060c11223efc2723b19cacf6605e433fa686ab5d4/coverage-7.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7bafc3fe1059463a8fdd97ca79972d6e2bf819d775c7d54991b5b1971201d6", size = 251493, upload-time = "2026-07-02T13:08:25.397Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2b/867faa17030a806114dae388b32a3fa929d8cd4bf39226fbc11f6e6bb705/coverage-7.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b713aa7fcf325a01d4184d848acb46fd84f78fdb0978470c636b23a06a753d91", size = 253406, upload-time = "2026-07-02T13:08:26.842Z" }, + { url = "https://files.pythonhosted.org/packages/94/c0/d789ce18f6605afc4895db75723424be2ef494282f77f61d8e5832923183/coverage-7.15.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38e6fba2d56652fdfaf0231f8f78aeb805234a912de25dc291ee5cce5b8faa4", size = 248512, upload-time = "2026-07-02T13:08:28.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/b2673c30739f4a2e06649a0a38ad8b093c4d865462dc7bab0e9524a2c3b1/coverage-7.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:884499f42e382675be80770391983b90e0c0c774d87dbeeebf5f991cf6612b20", size = 249532, upload-time = "2026-07-02T13:08:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/acd79e9a41beabee92b623afe4f30b549916f48566271475f2907e752828/coverage-7.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:840481b12e083dbcbafab14794a8781a958edf327c8d3d70b4eee42f9b8253aa", size = 247537, upload-time = "2026-07-02T13:08:31.173Z" }, + { url = "https://files.pythonhosted.org/packages/12/d4/2d301c4d1b3238d7c88b70ab9d13fd53ed9505662a7ff1b46ba1e2e4e3c3/coverage-7.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:276646e9481703d09f854f3b2f018f24e19fd7049ae670a92570043eb97203b1", size = 251348, upload-time = "2026-07-02T13:08:32.63Z" }, + { url = "https://files.pythonhosted.org/packages/35/bb/c67708b2bc00f32e12805ec23d5fa677a0a51652f449341a89f9d6b1b715/coverage-7.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4de4b4d3f5545aa6c60dc4efd9c63b5b5dcc3bf00fe83146b2bdfffb8f6613bd", size = 247806, upload-time = "2026-07-02T13:08:33.931Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6c/57c4f653c47a6e917748f8938e389e72fbcae44e3643cd906664f0477a13/coverage-7.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5c504097b2a89b1e85bc6070d920df77daec701337e3aeef2c17775a5dd0ca90", size = 248410, upload-time = "2026-07-02T13:08:35.189Z" }, + { url = "https://files.pythonhosted.org/packages/6c/94/bb083041aef828903668f134273f319f2bd49224962875359c52faa5497f/coverage-7.15.0-cp310-cp310-win32.whl", hash = "sha256:f6e80ed91f98316e86b9c137206b04b2bcfbffccbdff49bd2eb09dddb1cf14e0", size = 222588, upload-time = "2026-07-02T13:08:36.486Z" }, + { url = "https://files.pythonhosted.org/packages/ef/94/a09d8ee618956f626741b0734854bac4425a00e10c0565f5abca64e7e751/coverage-7.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3b3e22030f3f6f5e01a5ce69936552a5c0f6992b7698777377b99041961031f", size = 223214, upload-time = "2026-07-02T13:08:37.885Z" }, + { url = "https://files.pythonhosted.org/packages/ae/23/82e910835ef4b8391047025e1d53aa48d66029f444eb8b25373c849bf503/coverage-7.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:003fff99412ea848c0aaebcc78ed2b6ce7d8a1227ed17e68470672770b78a02a", size = 220662, upload-time = "2026-07-02T13:08:39.205Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0d/c7b213dde2f1579de5231062b386d8413f79c11667eb58c39319b25991da/coverage-7.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cbd804bf2784ce7b45114516050f346ecd50f960c4bb630a7ee9e1d78fa2118", size = 221168, upload-time = "2026-07-02T13:08:40.471Z" }, + { url = "https://files.pythonhosted.org/packages/33/77/d000aeedfac085088337b3c7becdad328474b1f8a9e4c9368a0c99605d68/coverage-7.15.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8773e15c23305b58882a4611fb9b2755977eae0dc2e515366a1b6c98866cc4c2", size = 251587, upload-time = "2026-07-02T13:08:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e0/86787c56b9df17afd370d5e293515dd4d9a107a561d13054873eefad8ecc/coverage-7.15.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f50e40081494c1dc4239ebb202014cbcc3306ea96fb6302a34c8cc0967fc5ae8", size = 253497, upload-time = "2026-07-02T13:08:43.387Z" }, + { url = "https://files.pythonhosted.org/packages/3f/02/181bc917359299c07dead6270f94e411151c8b60cec905c33499da69afe6/coverage-7.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daf96f37f5fc3a7b6c6da862eb4aee61c426bd63da236ed4a73ef0e503b4bca5", size = 255607, upload-time = "2026-07-02T13:08:44.897Z" }, + { url = "https://files.pythonhosted.org/packages/b9/35/ca5e7427699913da6788c4f910e73ab16c5f4b59ec5d3a999dce2a45112f/coverage-7.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:51aa20f6ae2788fd197747766edf4cd8234fd9423309b934257fa6b21a592723", size = 257563, upload-time = "2026-07-02T13:08:46.334Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4d/b8220bacc2fc3c4e9078e27c32e99fb411479a4718a72bdd00036a9891c8/coverage-7.15.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03d1f922757662eb7af586e77834792274cff776bc7b1d1a0b66a49ea9d84735", size = 251726, upload-time = "2026-07-02T13:08:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e4/2e145da1991d72189b9c3cf7eca05c716ee7080d099aaea6757cfc7df008/coverage-7.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a6d6acc9a7666245e6133dd15144ca038a85a9cd5026bb06d6bbae9e77440dc9", size = 253301, upload-time = "2026-07-02T13:08:49.5Z" }, + { url = "https://files.pythonhosted.org/packages/72/28/d2c841d698bf762e481f08bd4839d370246b6d9b61dab085a7b20b201a08/coverage-7.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ac2c4c27c7df851dc9a017c2d7de00b69147e84ba3d96f37a530b0b6fb51035", size = 251361, upload-time = "2026-07-02T13:08:51.304Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ed/55d9ffde994fba3897c0c783f77a7d053b0c18787f6892ed5b0aed73f469/coverage-7.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b761a1d504fd4bd1f20f418753964dca9f5862a511fc854dac58296b3b223671", size = 255129, upload-time = "2026-07-02T13:08:52.661Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c0/ecbf33b8c460ea2718aeb813e2df8140d0370e5f67261c31524ceb0a2a8d/coverage-7.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e43b045e11c16e897895758ae90e4a90cf99e93d58549e2f90c0e2272e155695", size = 251081, upload-time = "2026-07-02T13:08:54.188Z" }, + { url = "https://files.pythonhosted.org/packages/a9/de/fb87b4261f54448dd2b9504ef19a58be42cef0d9520595fbfe1219b15234/coverage-7.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:589b54513e901739f4b4582c705ce96b80c96f57641b1464607e2367a270e540", size = 251988, upload-time = "2026-07-02T13:08:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/df/27/3494d5f291b9a4cb868f73c11221a8bd2d5bd761a8f9acea61ff57128dd1/coverage-7.15.0-cp311-cp311-win32.whl", hash = "sha256:106781b8482749162d0b47056937ba0933508e5d9447f65a5e7d5c422f0d6bb4", size = 222754, upload-time = "2026-07-02T13:08:57.091Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ee/cd4847ebc9be6a9c0123d763645a6f1f3be6b8c58c962706368b79cbac07/coverage-7.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:821e92b3631d762a339695824cadbbc73020354eba2a23a551a99ad34938fbe6", size = 223225, upload-time = "2026-07-02T13:08:58.594Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/5011581aa7f2be498b97dcc7c9902192442a42f4f9a748aeadb3d6506b42/coverage-7.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:309990eb5fb8014b9f67cb211f7fd41876ec8a88a88d3ae76de0ed1d611e3640", size = 222774, upload-time = "2026-07-02T13:09:00.074Z" }, + { url = "https://files.pythonhosted.org/packages/2a/74/fd4c0901137c4f8d81a76ada99e43c65163b4c94a02ece107a4ec0c6b615/coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d", size = 220838, upload-time = "2026-07-02T13:09:02.084Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2e/2347583467bd7f0402635101a916961915cc68fce652cd0db5f173ea04fc/coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe", size = 221197, upload-time = "2026-07-02T13:09:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/f0/17/99fa688541ae1d6e84543a0e544f83de0c944815b63e9e7b1ed411d15036/coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c", size = 252705, upload-time = "2026-07-02T13:09:05.059Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/6a95a5cd83b74839017ef9cf48d2d8c9ae60af919e17a3f336e6f9f1b7bd/coverage-7.15.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:20d9ccc4ebd0edc434d86dfd2a1dd2a8efa6b6b3073d0485a394fee86459ebb4", size = 255441, upload-time = "2026-07-02T13:09:06.559Z" }, + { url = "https://files.pythonhosted.org/packages/67/f2/406f6c57d600f68185942422c4c00f1a3255d60aee6e5fd961425cd9987e/coverage-7.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c8a976c365c8cb12f0cbd099508772ea41fb5fa80657a8506df0e11bd278c5", size = 256556, upload-time = "2026-07-02T13:09:08.197Z" }, + { url = "https://files.pythonhosted.org/packages/74/8e/d3fa48489c15ecdec1ba48fd61f68798555dddd2f6716f9ad42adeb1a2a9/coverage-7.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f948fd5ba1b9cbca91f0ae08b4c1ce2b139509149a435e2585d056d57d70bf01", size = 258815, upload-time = "2026-07-02T13:09:09.691Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/2d40ddd110462c6a2769677cf7f1c119a52b45f568978fc6c98e4cc0dd0f/coverage-7.15.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f58185f06edf6ad68ec9fb155d63ef650c82f3fbd7e1770e2867751fb13158f4", size = 253117, upload-time = "2026-07-02T13:09:11.212Z" }, + { url = "https://files.pythonhosted.org/packages/51/c0/310782f0d7c3cb2b5ac05ba8d205fe91f24a36f6bf3256098f1782181c38/coverage-7.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02adc79a920c73c647c5d117f55747df7f2de94571884758ce8bc58e04f0a796", size = 254475, upload-time = "2026-07-02T13:09:13.029Z" }, + { url = "https://files.pythonhosted.org/packages/86/f7/702da6c275f8ae6ade423d2877243122932c9b27f5403003b9ef8c927d12/coverage-7.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6eb7c300fbed667fd6e3588eba71c1904cdb06110ca6fdf908c26bdd88b8e382", size = 252619, upload-time = "2026-07-02T13:09:14.699Z" }, + { url = "https://files.pythonhosted.org/packages/fb/84/c5b15a7e5ecba4e56218d772d99fe80a63e63f8d11f12783723a6005ab45/coverage-7.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b5fb23fa2de9dce1f5c36c09066d8fcda16cd96e8e26686caa2d7cb9b567d65c", size = 256689, upload-time = "2026-07-02T13:09:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/95/2f/c8b07559b57701230c61b23a953858c052890c12ef568d81780c6c46e92e/coverage-7.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cec79341dbe6281484024979976d0c7f22beae08b4a254655decd25d42cbe766", size = 252189, upload-time = "2026-07-02T13:09:17.828Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/6d2f049dd3fd3dbfd60b62ba6b2162a04009e2c002ce70b24cf3878dec7a/coverage-7.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c664c5444b1d970b1b2a450e21fb19ee5c9cfdf151ded2dda37260031cca0da", size = 254059, upload-time = "2026-07-02T13:09:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/ce/92/b0287a2c42031d25c628f815f89a3cd9f8268ee78bb1252c9356cda1c689/coverage-7.15.0-cp312-cp312-win32.whl", hash = "sha256:5f764a3fa339bde6b3aa97657f5a6a3a9451e4a5b4ea98a2892c773a43525f77", size = 222893, upload-time = "2026-07-02T13:09:20.812Z" }, + { url = "https://files.pythonhosted.org/packages/a9/69/e34c481915fecb499b3146975061dac528752e37706edc1804f32c822469/coverage-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:52f9a4d2c4c56c8848bc2f524916698354b0211488b38c49ad9ae54f6cafbff6", size = 223429, upload-time = "2026-07-02T13:09:22.315Z" }, + { url = "https://files.pythonhosted.org/packages/fe/98/6e878f0b571d32684ef3f38d7c03db241ca5b82a5da8a5391596a8f209c4/coverage-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:31e5c3e70c85307ea35a12964e2e40f56ca2ee4b1c8c721ccf4609d17071080b", size = 222810, upload-time = "2026-07-02T13:09:23.812Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/145a3748098bcc86b631a85408d2c3dc5c104e0bd86d605468239b25b6c4/coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782", size = 220863, upload-time = "2026-07-02T13:09:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/4ed55708fed2c64b63c9bc5715daef670872202101938869b7fe5d5fbb8f/coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430", size = 221230, upload-time = "2026-07-02T13:09:26.897Z" }, + { url = "https://files.pythonhosted.org/packages/7b/19/3a80b97d3b2a5c77a01ae359c6bed20c13738fe3d9380f08616d4fec0281/coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5", size = 252227, upload-time = "2026-07-02T13:09:28.543Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/b70062750686bd7da454da27927622f48bbac6990ac7a4c4a4653e7b0036/coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970", size = 254823, upload-time = "2026-07-02T13:09:30.177Z" }, + { url = "https://files.pythonhosted.org/packages/a9/09/dad6a75a2e561b9dc5086a8c5257a7591d584246f67e23e70d2995b89ab6/coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c", size = 256059, upload-time = "2026-07-02T13:09:31.979Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/b5d2941fa9564573d44b693a871ff3156f0c42cbefe977a09fa7fdc59971/coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84", size = 258190, upload-time = "2026-07-02T13:09:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1d/8e895bcde3c57ccd46d896dda5f2b3d5df761a1b0c6c9d450d175dedc632/coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389", size = 252456, upload-time = "2026-07-02T13:09:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/f6997da343ddeb959be82c3b05322793f92c071ad45f7cb8a96336e2dd5f/coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950", size = 254192, upload-time = "2026-07-02T13:09:37.445Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/a0bc09d032267b9da89d95a2d874cfbef2a5aebbf0e87cf7aba221d79a99/coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e", size = 252153, upload-time = "2026-07-02T13:09:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/54/c0/77fc233d9fba07b244c40948c53fe27308b8f21732fb3417f87fbd6fd992/coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e", size = 256310, upload-time = "2026-07-02T13:09:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/d5/24/601cecfb5825becacb8d45219a018a3b55b9dbaec624efdb0ea249d08be2/coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837", size = 251974, upload-time = "2026-07-02T13:09:42.733Z" }, + { url = "https://files.pythonhosted.org/packages/47/1e/6f45e5a5b3d5484318d368702af6716b5ab8913b0428bec981a562fcf296/coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37", size = 253745, upload-time = "2026-07-02T13:09:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4df027a77bd11d0e527f44c53557c76e54ad027413d0304252ea3a78d67e/coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b", size = 222902, upload-time = "2026-07-02T13:09:46.122Z" }, + { url = "https://files.pythonhosted.org/packages/a0/10/0355894d34e231f2c5449e71287e81a50793a325df2e2b027b7bcd9dfd19/coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629", size = 223444, upload-time = "2026-07-02T13:09:47.687Z" }, + { url = "https://files.pythonhosted.org/packages/06/ef/bb725f263befaaff851203ab338e68af15e195d7f7b5f323162532d9b6a8/coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21", size = 222839, upload-time = "2026-07-02T13:09:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9c/1e3ca54f72a3185ece06c58d871099898c48f0ed6430d17b6ab75f0d180a/coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324", size = 220906, upload-time = "2026-07-02T13:09:51.339Z" }, + { url = "https://files.pythonhosted.org/packages/09/37/f718613d83b274880382f6b67e78f3802549ae39b0b3e65ae5b5974df56e/coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8", size = 221239, upload-time = "2026-07-02T13:09:53.138Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/22bae91e0b75445f68d365c7643ed0aa4880bbf77450ee74ca65bdae53a7/coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092", size = 252286, upload-time = "2026-07-02T13:09:54.996Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/bec5e32aa508615d9d7a2790effb25fb4dc28606e995816afe400b25ece3/coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502", size = 254789, upload-time = "2026-07-02T13:09:56.678Z" }, + { url = "https://files.pythonhosted.org/packages/17/29/0e865435b4354e4a7c03b1b7920046d31d0a273d55decefea27e011cb9bf/coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2", size = 256135, upload-time = "2026-07-02T13:09:58.343Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/33a870b58a13325d62fc0a6c8f01fa0ff667cef60c7498e2382a147dfa18/coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e", size = 258449, upload-time = "2026-07-02T13:10:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/18/7b/6fffe596bf3ddba8462758d02c5dad730fd91055a6634aa2e4226229181a/coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888", size = 252313, upload-time = "2026-07-02T13:10:01.946Z" }, + { url = "https://files.pythonhosted.org/packages/58/1b/11468dd6c1676ab831a70cb9a8d4e198e8607fa0b7220ab918b73fe9bfbd/coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859", size = 254142, upload-time = "2026-07-02T13:10:04.065Z" }, + { url = "https://files.pythonhosted.org/packages/79/41/29328e21d16b1b95092c30dd700e08cf915bd3734f836df8f3bdb0e8fa9f/coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99", size = 252108, upload-time = "2026-07-02T13:10:06.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/de/05ccfb990439655b35afbfd8e0d13fe66677565a7d4eb38c3f5ef2635e1c/coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676", size = 256385, upload-time = "2026-07-02T13:10:08.141Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/486828a3d2695ea7a2609f17ff572f6b01905e608379440a11da4b8dffbe/coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6", size = 251923, upload-time = "2026-07-02T13:10:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/18/c7/03582b6715f078e5e558354c87616d945b9894cda2dace8e4009b17035e4/coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe", size = 253580, upload-time = "2026-07-02T13:10:12.052Z" }, + { url = "https://files.pythonhosted.org/packages/db/dc/9e578bbaf2ecb4959a81b7e7601ad8cca772cba2892e8d144cb749b4a71a/coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f", size = 223107, upload-time = "2026-07-02T13:10:13.994Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3e/c8c3b75d8dbe0e35f7b0cc3ff5e949fc59500f70b21d0398813f66740664/coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663", size = 223597, upload-time = "2026-07-02T13:10:15.906Z" }, + { url = "https://files.pythonhosted.org/packages/cd/bc/3cbc9fb036eb388519bccd521f783499c39b64256013fbc362782f196fe1/coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da", size = 223020, upload-time = "2026-07-02T13:10:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/28/00/199c4a8d656dff63102577a056c0fce2ff6a79e40adac092fc986c49cbf1/coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641", size = 221638, upload-time = "2026-07-02T13:10:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/9d0092c96a3d3a26951ecc7020826aa57bcb1b119ca81acbba996884ab13/coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa", size = 221903, upload-time = "2026-07-02T13:10:21.514Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b4/c0ca3028f42c9a08e51feb4561ef1192e5de99797cd1db5b04590c215bda/coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461", size = 263267, upload-time = "2026-07-02T13:10:23.261Z" }, + { url = "https://files.pythonhosted.org/packages/5f/aa/a375e3846e5d3c013dc600b2a3231089055c73d77f5393dd2192a8d64da6/coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72", size = 265390, upload-time = "2026-07-02T13:10:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/92/e1/5783cdabb797305e1c9e4809fea496d31834c51fa772514f73dc148bcfc9/coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd", size = 267811, upload-time = "2026-07-02T13:10:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/85/31/96d8bbf58b8e9193bc8389574a91a0db48355ee98feb66aa6bf8d1b32eea/coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5", size = 268928, upload-time = "2026-07-02T13:10:29.242Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7a/5294567e811a1cb7eda93140c628fa050d66189da28da320f93d1d815c73/coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007", size = 262378, upload-time = "2026-07-02T13:10:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/69/3f/3f48538421f899f28946f90a3d272136a4686e1abf461cc9249a783ee0f3/coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9", size = 265263, upload-time = "2026-07-02T13:10:32.942Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d3/092df15efcab8a9c1467ee960eb8019bbad3f9300d115d89ea6195f369ff/coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f", size = 262866, upload-time = "2026-07-02T13:10:35.104Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ab/0254d2b88665efb2c57ad368cc77ab5de3435bd8d5add4729c1b0e79431e/coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571", size = 266599, upload-time = "2026-07-02T13:10:37.05Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/1cfa4023e489ce6fbc7be4a5d442dbc375edb4f4fda39a352cedb53263c2/coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8", size = 261714, upload-time = "2026-07-02T13:10:38.966Z" }, + { url = "https://files.pythonhosted.org/packages/b7/eb/fee5c8665656be63f497418d410484637c438172568688e8ac92e06574e7/coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68", size = 264025, upload-time = "2026-07-02T13:10:40.789Z" }, + { url = "https://files.pythonhosted.org/packages/ab/99/63005db722f91edc81abc16302f9cc2f6228c1679e46e15be9ae144b14d0/coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b", size = 223413, upload-time = "2026-07-02T13:10:42.597Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e8/2bc6181c4fb06f1a6b981eb85330cc57bfad7e3f710fc9c9d350013ba228/coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080", size = 224245, upload-time = "2026-07-02T13:10:44.47Z" }, + { url = "https://files.pythonhosted.org/packages/79/b8/4d959bf9cc45d0cfed2f4d35cafcab978cdb6ea02eb5100009cd740632a3/coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5", size = 223558, upload-time = "2026-07-02T13:10:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, ] [package.optional-dependencies] @@ -725,7 +730,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -764,88 +769,88 @@ wheels = [ [[package]] name = "greenlet" -version = "3.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/21/117c8710abb7f146d804a124c07eb5964a60b90d02b72452885aecc18efa/greenlet-3.5.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7eacb17a9d41538a2bc4912eba5ef13823c83cb69e4d141d0813debe7163187f", size = 283510, upload-time = "2026-05-20T13:12:26.475Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f7/6762a56fa5f6c2295c449c6524e10ce481e381c994cc44d9d03aef0700fb/greenlet-3.5.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5cc9606aa5f4e0bde0d3bd502b44f743864c3ffa5cfa1011b1e30f5aa02366f", size = 599696, upload-time = "2026-05-20T14:00:02.906Z" }, - { url = "https://files.pythonhosted.org/packages/0f/05/85a511e68ee109aff0aa00b4b497806091dd2d82ce209e49c6e801bd5d92/greenlet-3.5.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3d35f87c7253b715d13d679e0783d845910144f282cb939fe1ba4ac8616269c", size = 612618, upload-time = "2026-05-20T14:05:39.202Z" }, - { url = "https://files.pythonhosted.org/packages/2e/19/60df45065b2981ff894fdd51e7c99a3a4b107412822b083d88d5d528f663/greenlet-3.5.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:00929c98ec525fd9bf075875d8c5f6a983a90906cdf78a66e6de2d8e466c2a19", size = 619237, upload-time = "2026-05-20T14:09:06.421Z" }, - { url = "https://files.pythonhosted.org/packages/89/b8/8b83d18ae07c46c019617f35afd7b47aab7f9b4fbb12fc637d681e10bdd8/greenlet-3.5.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:540dae7b956209af4d70a3be35927b4055f617763771e5e84a5255bea934d2f5", size = 612947, upload-time = "2026-05-20T13:14:23.469Z" }, - { url = "https://files.pythonhosted.org/packages/26/9a/4ba4c2bc9d9df5f41bb8943fb7bb11e440352e6b9c2e36716b6e85f8b82d/greenlet-3.5.1-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:001775efe7b8e758861294c7a27c28af87f3f3f1c20468a2bc618c45b346c061", size = 415653, upload-time = "2026-05-20T14:01:36.999Z" }, - { url = "https://files.pythonhosted.org/packages/5d/14/ad1f9fc9b82384c010212464a3702bd911f95dab2f1180bc6fbcfb1f958c/greenlet-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed8cdb691169715a9a492844a83246f090182247d1a5031dc78a403f68ba1e97", size = 1571425, upload-time = "2026-05-20T14:02:22.671Z" }, - { url = "https://files.pythonhosted.org/packages/46/1c/43b8203cf10f4292c9e3d270e9e5f5ade79115a0a0ca5ea6f1be5f8915a7/greenlet-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d59e840387076a51016777a9328b3f2c427c6f9208a6e958bad251be50a648d", size = 1638688, upload-time = "2026-05-20T13:14:30.026Z" }, - { url = "https://files.pythonhosted.org/packages/ac/6e/0344b1e99f58f71715456e46492101fd2daa408957b8186ade0a4b515da7/greenlet-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:b9152fca4a6466e114aaec745ae61cba739903a109754a9d4e1262f01e9259b1", size = 237763, upload-time = "2026-05-20T13:11:35.659Z" }, - { url = "https://files.pythonhosted.org/packages/42/3c/ff890b466eaba2b0f5e6bdfff025f8c75f41b8ffdc3dbc3d24ad261e764a/greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f", size = 284764, upload-time = "2026-05-20T13:09:10.204Z" }, - { url = "https://files.pythonhosted.org/packages/81/0e/5e5457be3d256918f6a4756f073548a3f0190836e2cc94aa6d0d617a940b/greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2", size = 603479, upload-time = "2026-05-20T14:00:04.757Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e1/f89a21d58d308298e6f275f13a1b472ed96c680b601a371b08be6a725989/greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33", size = 615495, upload-time = "2026-05-20T14:05:40.87Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/8fd452fd81adb9ec79c8275c1375702ab0fd6bee4952da12eaa09b9508d8/greenlet-3.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ebeb75c81211f5c702576cf81f315e77e23cfdb2c7c6fcb9dd143e6de35c360", size = 623515, upload-time = "2026-05-20T14:09:07.853Z" }, - { url = "https://files.pythonhosted.org/packages/75/de/af6cef182862d2ccd6975440d21c9058a77c3f9b469abf94e322dfd2e0e3/greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563", size = 614754, upload-time = "2026-05-20T13:14:24.947Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bc/c318aa9f3ffc77320fddcee3d892be957b42e2ff947198d9450b004f3a38/greenlet-3.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:017a544f0385d441e88714160d089d6900ef46c9eff9d99b6715a5ef2d127747", size = 418439, upload-time = "2026-05-20T14:01:38.446Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c6/50e520283a9f19388a7326b05f9e8637e566003475eacaadad04f558c68d/greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071", size = 1574097, upload-time = "2026-05-20T14:02:24.003Z" }, - { url = "https://files.pythonhosted.org/packages/21/1c/13abd1f4860d987fa5e1170a01930d6e6cd40d328de487a3c9fdaff0ffd0/greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c", size = 1641058, upload-time = "2026-05-20T13:14:31.83Z" }, - { url = "https://files.pythonhosted.org/packages/f5/56/5f332b7705545eac2dc01b4e9254d24a793f2656d55d5cc6b94ee59d22ae/greenlet-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e", size = 238089, upload-time = "2026-05-20T13:14:03.229Z" }, - { url = "https://files.pythonhosted.org/packages/d9/a9/a3c2fa886c5b94863fb0e61b3bc14610b7aa94cf4f17f8741b11708305fc/greenlet-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:cc6ab7e555c8a112ad3a76e368e86e12a2754bcae1652a5602e133ec7b635523", size = 234989, upload-time = "2026-05-20T13:08:27.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" }, - { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" }, - { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" }, - { url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094, upload-time = "2026-05-20T14:09:09.18Z" }, - { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" }, - { url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782, upload-time = "2026-05-20T14:01:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" }, - { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" }, - { url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" }, - { url = "https://files.pythonhosted.org/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", size = 235580, upload-time = "2026-05-20T13:08:45.056Z" }, - { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" }, - { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" }, - { url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" }, - { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" }, - { url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" }, - { url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" }, - { url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/2d80842910da44f78c286532d084b8a5c3717c844ae80ceb3858738ae89a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", size = 667767, upload-time = "2026-05-20T14:09:12.15Z" }, - { url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d3/dad2eecedfbb1ed7050a20dcfae40c1442b74bc7423608be2c7e03ee7133/greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", size = 470786, upload-time = "2026-05-20T14:01:42.064Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" }, - { url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" }, - { url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" }, - { url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" }, - { url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" }, - { url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" }, - { url = "https://files.pythonhosted.org/packages/8c/46/5987dcd1a2570ba84f3b187536b2ca3ae97613387e57f5cfa99df068fe5e/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", size = 656607, upload-time = "2026-05-20T14:09:13.949Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c1/6da0a9ddcc29d7e51ef14883fa3dc1e53b3f4ffba00582106c7bf55da1d8/greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", size = 488287, upload-time = "2026-05-20T14:01:43.143Z" }, - { url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" }, - { url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" }, - { url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" }, - { url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" }, - { url = "https://files.pythonhosted.org/packages/dc/74/807a047255bf1e09303627c46dc043dca596b6958a354d904f32ab382005/greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", size = 672962, upload-time = "2026-05-20T14:09:15.532Z" }, - { url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" }, - { url = "https://files.pythonhosted.org/packages/76/32/19d4e13225193c29b13e308015223f7d75fd3d8623d49dd19040d2ce8ec1/greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", size = 476047, upload-time = "2026-05-20T14:01:44.39Z" }, - { url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" }, - { url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" }, - { url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" }, - { url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" }, - { url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" }, - { url = "https://files.pythonhosted.org/packages/c9/9d/1dcdf7b95ab3cf8c7b6d7277c18a5e167312f2b362ddfcc5d5e6d8d84b43/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", size = 659998, upload-time = "2026-05-20T14:09:16.912Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/c4959664fc231d587d66d8e81f2095e98056ba1954beafdcbe635e251052/greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", size = 494470, upload-time = "2026-05-20T14:01:45.611Z" }, - { url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" }, - { url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" }, - { url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" }, - { url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" }, +version = "3.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/f1/fbbfef6af0bad0548f09bc28948ea3c275b4edb19e17fc5ca9900a6a634d/greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1", size = 200270, upload-time = "2026-06-26T19:28:24.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/a1/1f7f0c555f5858fd2906fe9f7b0a3554fddb85cb70df7a6aaec41dc292c2/greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c", size = 285838, upload-time = "2026-06-26T18:21:05.167Z" }, + { url = "https://files.pythonhosted.org/packages/0a/29/be9f43ed61677a5759b38c8a9389248133c8c731bbfc0574ecdff66c99fc/greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04", size = 602342, upload-time = "2026-06-26T19:07:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/ba41c97ec36aa4b3ec25e5aa691d79561254805fad7f2f826dd6770587e2/greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce", size = 615541, upload-time = "2026-06-26T19:10:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/2e/8c/231ca675b0df779816950ca66b40b1fa14dbff4a0ed9814a9a29ec399140/greenlet-3.5.3-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8", size = 622473, upload-time = "2026-06-26T19:24:12.786Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c7/28747042e1df8a9cd120a1ebe15529fc4be3b486e13e8d551ff307a82412/greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec", size = 615675, upload-time = "2026-06-26T18:32:14.444Z" }, + { url = "https://files.pythonhosted.org/packages/81/fe/dd97c483a3ff82849196ccd07851600edd3ac9de74669ca8a6022ada9ea1/greenlet-3.5.3-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71", size = 418421, upload-time = "2026-06-26T19:25:34.503Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a8/b85525a6c8fba9f009a5f7c8df1545de8fb0f0bf3e0179194ef4e500317f/greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8", size = 1575057, upload-time = "2026-06-26T19:09:00.264Z" }, + { url = "https://files.pythonhosted.org/packages/03/79/fb76edb218fe6735ab0edeba176c7ab80df9618f7c02ce4208979f3ae7db/greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702", size = 1641692, upload-time = "2026-06-26T18:31:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/6b/79/86fe3ee50ed55d9b3907eecd3208b5c3fe8a79515519aae98b4753c3fa1d/greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db", size = 238742, upload-time = "2026-06-26T18:20:40.758Z" }, + { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a0/68afd1ebad40db87dac0a28ffa120726b98bf9c7c40c481b0f63c105d298/greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb", size = 626155, upload-time = "2026-06-26T19:24:14.44Z" }, + { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" }, + { url = "https://files.pythonhosted.org/packages/07/7f/e327d912239ec4b3b49999e3967389bcf1ee8722b9ee9194d2752ecd558a/greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c", size = 421083, upload-time = "2026-06-26T19:25:35.804Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" }, + { url = "https://files.pythonhosted.org/packages/fb/96/b9820295576ef18c9edc404f10e260ae7215ceaf3781a54b720ed2627862/greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44", size = 237630, upload-time = "2026-06-26T18:24:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, + { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, + { url = "https://files.pythonhosted.org/packages/fb/fb/d97dc261209c80744b7c8132693a30d70ec6e7315e632cb0a10b3fec94dd/greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23", size = 622351, upload-time = "2026-06-26T19:24:16.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/e5fee13cbbd0e8de312d9a146584b8a51891c68847330ef9dc8b5109d23f/greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c", size = 425395, upload-time = "2026-06-26T19:25:37.144Z" }, + { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, + { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, + { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/6fea0e3d6600f785069481ee637e09378dd4118acdfd38ad88ae2db31c98/greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d", size = 238211, upload-time = "2026-06-26T18:22:37.671Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/2f5b1adf256d039f5dab8005de8d3d7ad2b0070a3219c0e036b3fbfeb440/greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1", size = 625580, upload-time = "2026-06-26T19:24:18.344Z" }, + { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/ab7fc9e543e44d6879b0a6ef9a4b2188940fd180cc65d6f646883ddf7201/greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda", size = 427933, upload-time = "2026-06-26T19:25:38.219Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/220a7f5824a64a60443fc03b39dfac4ea63a7fb6d481efa27eafa928e7f4/greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4", size = 238141, upload-time = "2026-06-26T18:22:48.507Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, + { url = "https://files.pythonhosted.org/packages/86/a9/73fa62893d5b84b4205544e6b673c654cc43aa5b9899bac00f04d64af73d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814", size = 670657, upload-time = "2026-06-26T19:24:19.967Z" }, + { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, + { url = "https://files.pythonhosted.org/packages/29/7e/2ffce64929fb3cab7b65d5a0b20aaf9764e227681d731b041077fc9a525a/greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260", size = 473497, upload-time = "2026-06-26T19:25:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, + { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/68d0983e79e02138f64b4d303c500c27ddb48e5e77f3debb80888a921eae/greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605", size = 239549, upload-time = "2026-06-26T18:22:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, + { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, + { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/8faec206b851c22b1733545fda900829a1f3f5b1c78ae7e0fb3dba57d9f4/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d", size = 659582, upload-time = "2026-06-26T19:24:21.357Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, + { url = "https://files.pythonhosted.org/packages/b4/55/50c19e49f8045834ada71ef12f8ad048eba8517c6aa41161bed676328fae/greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0", size = 491037, upload-time = "2026-06-26T19:25:40.672Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, + { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/aa/952cf28c2ff949a8c971134fb43854dd7eaa737218723aaef758f8c9aead/greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357", size = 674261, upload-time = "2026-06-26T19:24:22.79Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/b00d6f5e63e531a93562b2ec1a4c320fbee91f580fc42e6417af69d706e5/greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128", size = 480322, upload-time = "2026-06-26T19:25:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, + { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e8/883785b44c5780ed71e83d3e4437e710470be17a2e181e8b601e2da0dc4a/greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227", size = 240085, upload-time = "2026-06-26T18:23:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, + { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/0e0938a75115b939d42733a2a12e1d349653c9531fe6fe563e8a681f04e6/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91", size = 663706, upload-time = "2026-06-26T19:24:24.312Z" }, + { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/5b/41/35d1c678cdb3c3b9e6bee691728e563cfb294202b23c7a4c3c2ccc343589/greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d", size = 498803, upload-time = "2026-06-26T19:25:43.063Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, + { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/ca7d15afbdc397e3401134c9e1800d51d12b829661786187a4ad08fe484f/greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31", size = 242586, upload-time = "2026-06-26T18:23:37.93Z" }, ] [[package]] @@ -858,11 +863,11 @@ wheels = [ [[package]] name = "humanize" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/ea/13a1ef3c12d12662905801495283530251918b70d62d368f1d2e0272c70d/humanize-4.16.0.tar.gz", hash = "sha256:7dc2244a2f84a4bfb1d36c37bac80cd78e35cdc5c119206d87b018e1445f3a3f", size = 89515, upload-time = "2026-06-30T16:17:29.859Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/0b7365d30fed43e7a3449aba1fe20a0a7174d9cf13e282af4e69ac825441/humanize-4.16.0-py3-none-any.whl", hash = "sha256:353eb2f34c09d098b2880eee8bef21832eae6d174f48c5762fff7e5fcb74d01d", size = 137209, upload-time = "2026-06-30T16:17:28.36Z" }, ] [[package]] @@ -882,11 +887,11 @@ wheels = [ [[package]] name = "idna" -version = "3.17" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -1472,63 +1477,75 @@ wheels = [ [[package]] name = "msgpack" -version = "1.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, - { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, - { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, - { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, - { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, - { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, - { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, - { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, - { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, - { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, - { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, - { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, - { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, - { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, - { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, - { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, - { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, - { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, - { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, - { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, - { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, - { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, - { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, - { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" }, + { url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, ] [[package]] @@ -1551,36 +1568,36 @@ sdist = { url = "https://files.pythonhosted.org/packages/78/ec/ac9905ccab8774b64 [[package]] name = "nodejs-wheel-binaries" -version = "24.15.0" +version = "24.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/70/a1e4f4d5986768ab90cc860b1cc3660fd2ded74ca175a900a5c29f839c7d/nodejs_wheel_binaries-24.15.0.tar.gz", hash = "sha256:b43f5c4f6e5768d8845b2ae4682eb703a19bf7aadc84187e2d903ed3a611c859", size = 8057, upload-time = "2026-04-19T15:48:16.899Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/22/2a5beb4e21417c73233d9f65cf6f3e96e891b80d2f550a8f630ebc6b88c6/nodejs_wheel_binaries-24.16.0.tar.gz", hash = "sha256:c973cb69dc5fd16e6f6dc6e579e2c3d5534e2a1f57619dddf5ba070efa7dde37", size = 8056, upload-time = "2026-05-30T16:52:09.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/66/54051d14853d6ab4fb85f8be9b042b530be653357fb9a19557498bc91ab7/nodejs_wheel_binaries-24.15.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:a6232fa8b754220941f52388c8ead923f7c1c7fdf0ea0d98f657523bd9a81ef4", size = 55173485, upload-time = "2026-04-19T15:47:34.561Z" }, - { url = "https://files.pythonhosted.org/packages/ad/5f/66acada164da5ca10a0824db021aa7394ae18396c550cd9280e839a43126/nodejs_wheel_binaries-24.15.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:001a6b62c69d9109c1738163cca00608dd2722e8663af59300054ea02610972d", size = 55348100, upload-time = "2026-04-19T15:47:40.521Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2d/0cbd5ff40c9bb030ca1735d8f8793bd74f08a4cbd49100a1d19313ea57ab/nodejs_wheel_binaries-24.15.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0fbc48765e60ed0ff30d43898dbf5cadbadf2e5f1e7f204afc2b01493b7ebce6", size = 59668206, upload-time = "2026-04-19T15:47:46.848Z" }, - { url = "https://files.pythonhosted.org/packages/da/d5/91ac63951ec75927a486b83b8cafe650e360fa70ac01dc94adfb32b93b97/nodejs_wheel_binaries-24.15.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:20ee0536809795da8a4942fc1ab4cbdebbcaaf29383eab67ba8874268fb00008", size = 60206736, upload-time = "2026-04-19T15:47:52.668Z" }, - { url = "https://files.pythonhosted.org/packages/db/72/dc22776974d928869c0c30d23ee98ed7df254243c2df68f09f5963e8e8b8/nodejs_wheel_binaries-24.15.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1fade6c214285e72472ca40a631e98ff36559671cd5eefc8bf009471d67f04b4", size = 61720456, upload-time = "2026-04-19T15:47:58.325Z" }, - { url = "https://files.pythonhosted.org/packages/01/0a/34461b9050cb45ee371dccdefc622aef6351506ea2691b08fc761ca67150/nodejs_wheel_binaries-24.15.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3984cb8d87766567aee67a49743227ab40ede6f47734ec990ff90e50b74e7740", size = 62326172, upload-time = "2026-04-19T15:48:04.094Z" }, - { url = "https://files.pythonhosted.org/packages/c9/17/09252bf35672dba926649d59dfe51443a0f6955ad13784e91131d5ec82a2/nodejs_wheel_binaries-24.15.0-py2.py3-none-win_amd64.whl", hash = "sha256:a437601956b532dcb3082046e6978e622733f90edc0932cbb9adb3bb97a16501", size = 41543461, upload-time = "2026-04-19T15:48:09.332Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7e/b649777d148e1e0c2ce349156603cdb12f7ed99921b95d93717393650193/nodejs_wheel_binaries-24.15.0-py2.py3-none-win_arm64.whl", hash = "sha256:bdf4a431e08321a32efc604111c6f23941f87055d796a537e8c4110daecad23f", size = 39233248, upload-time = "2026-04-19T15:48:13.326Z" }, + { url = "https://files.pythonhosted.org/packages/83/d1/68b43b53cd0fa83ae6fd406705023ca988d9e0ca41c724d82e66fbeb2ef6/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:d9f8f677dcf30e37ac244f07869726abe043f01eb0f45722b1df31cc2af7093c", size = 55666374, upload-time = "2026-05-30T16:51:39.588Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b2/40a989159599080da485de966c4c2d207e852ac7aa7864702626d96c8bf5/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:3d0370fe7120ce9697a4f60d40480d2bd8808d9f30131458d5afc0040d4e5a51", size = 55838487, upload-time = "2026-05-30T16:51:43.383Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a7/cd42174fb5ff6faff7fa8d326a18914d8f232098ab5de055b57c16fa13ca/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:85dc92bbb79c851569c5925dcc2a4c915a034efab375f99e4e7e6bbe9cca8342", size = 60179540, upload-time = "2026-05-30T16:51:47.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/c8a1f9ae140aa28df8744d984d01d4b3af7cdd6555af12127f40ceb45a7d/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2f3036292811514ba847b3708492644764f88a833ac425c5f55007014308ddfd", size = 60716262, upload-time = "2026-05-30T16:51:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/7c35b3737f59e36d0249c265397b7bff570519b95301d6e16ea361e904ad/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:db8a8a76ebd2b28ecbfc9ad464baa3707241b9e050a30e2efdf6f60c0f886502", size = 62230592, upload-time = "2026-05-30T16:51:55Z" }, + { url = "https://files.pythonhosted.org/packages/04/96/d931255cf9d11a84d6b54d882dba7434646467d568ccf070ea3418638df3/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f1a3d8f7b4491cbbd023ba3fc4e901fcca2d9fb80d57f24ba3890de8b1dbac03", size = 62841759, upload-time = "2026-05-30T16:51:59.407Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7b/8b7a3f41bc255411be30b6d7d288aab8ffd9ea2055db8555ced3548007b9/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_amd64.whl", hash = "sha256:bb136be9944f0662dcf1120f45193a6b75b13fac378971a95cc42c9f879a81aa", size = 42027734, upload-time = "2026-05-30T16:52:03.348Z" }, + { url = "https://files.pythonhosted.org/packages/17/66/1ed71f1f529b8ca727d42c7ceb9db0bef145ce4a13dfc86fb50aa44f3be6/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:8308940b5edd0a50dc5267ea36ba21c9f668e83fe0d9f293937174d3a7e31c36", size = 39714528, upload-time = "2026-05-30T16:52:06.421Z" }, ] [[package]] name = "oxipng-pybind" -version = "10.1.1.post1" +version = "10.1.1.post3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/38/c74f1678f29ddc43ee5ad9da4975407583bb7f541e0d832d23700e741c5a/oxipng_pybind-10.1.1.post1.tar.gz", hash = "sha256:79bf22b6db5cca52428d23c930b9d4d11e7bff38bd96c94e122df83326c9d143", size = 152404, upload-time = "2026-05-28T11:20:14.585Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/eb6b06f24b765f0dcb7d85c2834541fae9ce6f0be85e2c8eab7a636f1ec0/oxipng_pybind-10.1.1.post3.tar.gz", hash = "sha256:ca9d9cfa015a1bc0fcb341eb5e7c08ef8cb6577917d6d72eee70a10087e076b2", size = 187238, upload-time = "2026-07-03T22:32:24.849Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/3b/863792fde6bec66e1ddad01110780b3b6edbcd8079b8e6f841870b822d1a/oxipng_pybind-10.1.1.post1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8e883cba03d1e3667d8457691744f77de774d9369a79917bbef3fa8ccb6a140c", size = 522823, upload-time = "2026-05-28T11:20:00.856Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d0/1fd1c37bf4b0def7883e412d6ab8c480b734b7ecfe36db5caccb6d75eb55/oxipng_pybind-10.1.1.post1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6dc2a4215d2fc10aa4f12271102c3639eb22c8d13ac8c8d26d240f5a0c40539", size = 489228, upload-time = "2026-05-28T11:20:02.487Z" }, - { url = "https://files.pythonhosted.org/packages/42/a1/42818e1b322a220f4e18d753b1574cca138fdb73fdb6ecd6063892662731/oxipng_pybind-10.1.1.post1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2f4d5063d8aaf93f940377375dc49b677a3818ad0758810756906e0f7b331fa7", size = 527090, upload-time = "2026-05-28T11:20:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/69/51/7658d4370e4ab9d3a08f9d1f4210a34fca8d3636aa40b953be8983b23e6e/oxipng_pybind-10.1.1.post1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ecc858000bb5860fce871f4d958f50d677847657acba1d0a1a717bdfeeb71ea8", size = 555599, upload-time = "2026-05-28T11:20:05.203Z" }, - { url = "https://files.pythonhosted.org/packages/3f/cd/571b5dc1d0e8f4b8cac620013c0076d9f47257d0e6bfcc881a9db7baa632/oxipng_pybind-10.1.1.post1-cp310-abi3-win_amd64.whl", hash = "sha256:cba36d82f6a7b080ca89ae83dee5dd6f3bb2879e91541b24415cad51fcf59be1", size = 456117, upload-time = "2026-05-28T11:20:06.508Z" }, - { url = "https://files.pythonhosted.org/packages/c2/34/1b005a569dd696e78edf7566d5f5f34db91b0a7ea6c92f422b15ccc33aa0/oxipng_pybind-10.1.1.post1-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3153a586a8dc12409dfc692f4aee78060e8c8ec204b0f33dfa9928518a04ab42", size = 522500, upload-time = "2026-05-28T11:20:07.898Z" }, - { url = "https://files.pythonhosted.org/packages/8d/fd/810a8bd08fc1639c50978d341d6e7e22e302347788afa20f6bb64ae8c4c3/oxipng_pybind-10.1.1.post1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:ad2272dd2b3087a35646f81aa89359b2b994a95959c6e5a2c50733f6e5f5ecd6", size = 488803, upload-time = "2026-05-28T11:20:09.122Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e7/cfb665747fb47b351659d5840577286079dd453a392ae1fb41eb67c3f2c7/oxipng_pybind-10.1.1.post1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6829b4ed103846697b43ecec289e5b0a171f4d26b903df446c1da653da2caf52", size = 526286, upload-time = "2026-05-28T11:20:10.442Z" }, - { url = "https://files.pythonhosted.org/packages/93/b0/ec33eea1f406a5aa9f92ca49dcc2e0fed0a03e38dc11db56f7eca706b984/oxipng_pybind-10.1.1.post1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16164129c684324995bea08fd4b2dfad2d9fa853e9dfa736db63e42744332dd2", size = 554024, upload-time = "2026-05-28T11:20:11.843Z" }, - { url = "https://files.pythonhosted.org/packages/27/66/dbaf080fd8849138733f61d773cb8512024444755b840cc5af30cdef7eea/oxipng_pybind-10.1.1.post1-cp311-abi3-win_amd64.whl", hash = "sha256:61f6d11d0ce22661edcc6fe587f249c98c1aa65333aa0e795a7d6d4d8eaf708b", size = 455504, upload-time = "2026-05-28T11:20:13.301Z" }, + { url = "https://files.pythonhosted.org/packages/23/0d/b0c330b7df466a34bd1450d8a948c38b2b1c5a16ca180060d0eb71b2204a/oxipng_pybind-10.1.1.post3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:04849d2d0f2014836c9e5031b4806684e5c8671e1f32aa47fb1995e1212ca3b4", size = 516855, upload-time = "2026-07-03T22:32:08.104Z" }, + { url = "https://files.pythonhosted.org/packages/c2/72/45ff201144278aa0a41a4acb38de46b0cad7fda2bdb048987e6f08db7670/oxipng_pybind-10.1.1.post3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:6fe7a719572d0135c3b512c877a2104c0ff287d3eda9e114492649ad6506eef5", size = 487366, upload-time = "2026-07-03T22:32:09.807Z" }, + { url = "https://files.pythonhosted.org/packages/8b/8e/1a829ef08a7c89e788988f7120aeebe859d6a43c99bb860a02858aa8d0a0/oxipng_pybind-10.1.1.post3-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2dfc6b40abebd58ee643a09d084c7977f9951ea3e56515f8cb9c96a4c09e3975", size = 531147, upload-time = "2026-07-03T22:32:11.679Z" }, + { url = "https://files.pythonhosted.org/packages/79/04/33c4272d73aedc99bef561091623a43d62f3a6de942e47e71943cd652f0e/oxipng_pybind-10.1.1.post3-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ed13db1dcc530ebeeb0d36fb422f945017edf4dee50304182c23665ec9d466be", size = 559186, upload-time = "2026-07-03T22:32:13.366Z" }, + { url = "https://files.pythonhosted.org/packages/16/bc/819547edcd3cab84517672d214dbeb3caa094bcbf4183c1888bbc9c68b1a/oxipng_pybind-10.1.1.post3-cp310-abi3-win_amd64.whl", hash = "sha256:dc7313b75855047499318897a58d761e5883c6a98fa119010ab2a100fc1b785e", size = 461254, upload-time = "2026-07-03T22:32:15.102Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c0/c15f18e7a1b5528e84d453c12c54ee95754a12452b3328c0f5195789eea8/oxipng_pybind-10.1.1.post3-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0de38c64cef6551c94481f9953e2edddd8d9c7ae5387103ff9e0e3c86bfd29c4", size = 515594, upload-time = "2026-07-03T22:32:16.737Z" }, + { url = "https://files.pythonhosted.org/packages/37/91/acf8bf94a762677fe0101e35c45472ee173717ab7395654b947e9a52b4a5/oxipng_pybind-10.1.1.post3-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:f20745f1c9c7e88ab2056442a2184d3ab619c9108e3771169aeb559b85046b9a", size = 486006, upload-time = "2026-07-03T22:32:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e1/338fccf1a0991889bd72b2e346f46935e95abf19e7002a7fc7132c4b5a39/oxipng_pybind-10.1.1.post3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a2d473e6333f90f02d1b400c890edac745d1f95cb27aaa6521a77a9a14a2525a", size = 528312, upload-time = "2026-07-03T22:32:20.167Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/5bd5b6510567c64563967d38d36b9d7aec62df48f3a17299f6fe63e338db/oxipng_pybind-10.1.1.post3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b5b441203f1675b42d0049aa000cc1c5fd672c1ae8604f86c89a880ba60a9751", size = 557812, upload-time = "2026-07-03T22:32:21.937Z" }, + { url = "https://files.pythonhosted.org/packages/11/91/5b6c5661dc6abb2df1b7ed3932c483c9c842aed5fd6148644a8fb48bfe8b/oxipng_pybind-10.1.1.post3-cp311-abi3-win_amd64.whl", hash = "sha256:8de00b373808def0f9090670d429ae3c2e05fefe1b0307a6e60290355f44394c", size = 460466, upload-time = "2026-07-03T22:32:23.347Z" }, ] [[package]] @@ -1612,7 +1629,7 @@ wheels = [ [[package]] name = "picopt" -version = "6.5.2" +version = "6.6.0" source = { editable = "." } dependencies = [ { name = "confuse" }, @@ -1671,7 +1688,6 @@ test = [ { name = "coverage", extra = ["toml"] }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, - { name = "pytest-gitignore" }, ] [package.metadata] @@ -1692,7 +1708,7 @@ requires-dist = [ { name = "rich", specifier = "~=15.0" }, { name = "rich-argparse", specifier = "~=1.8" }, { name = "ruamel-yaml", specifier = ">=0.18,<1.0" }, - { name = "treestamps", specifier = "~=4.0.1" }, + { name = "treestamps", specifier = "~=4.1.0" }, { name = "typing-extensions", specifier = "~=4.13" }, ] @@ -1717,7 +1733,7 @@ docs = [ lint = [ { name = "basedpyright", specifier = "~=1.38" }, { name = "codespell", specifier = "~=2.4" }, - { name = "complexipy", specifier = "~=5.1" }, + { name = "complexipy", specifier = "~=6.0" }, { name = "icecream", specifier = "~=2.1" }, { name = "mbake", specifier = "~=1.4.5" }, { name = "pathspec", specifier = "~=1.1.0" }, @@ -1731,7 +1747,6 @@ test = [ { name = "coverage", extras = ["toml"], specifier = "~=7.0" }, { name = "pytest-asyncio", specifier = "~=1.0" }, { name = "pytest-cov", specifier = "~=7.0" }, - { name = "pytest-gitignore", specifier = "~=1.3" }, ] [[package]] @@ -1745,148 +1760,151 @@ wheels = [ [[package]] name = "pikepdf" -version = "10.7.2" +version = "10.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "lxml" }, { name = "packaging" }, { name = "pillow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/14/3dc2afd965f0e4a032dbf7581bc88634c1b8717266f6d6067d4576278911/pikepdf-10.7.2.tar.gz", hash = "sha256:cddbe05a2398b19d99fc24aaab563695d05025a416f02deb4c2ac0a0068e3f65", size = 23666284, upload-time = "2026-05-25T07:50:49.336Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/f9/c4a078b85939b226b31f13b0923fa5b5ddf7e6e2c2f9577a61ea6de5232b/pikepdf-10.7.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:2df9035909e95d961c98da4df98144f30a436ac4417210475971affb4b7b0824", size = 4370665, upload-time = "2026-05-25T07:49:35.811Z" }, - { url = "https://files.pythonhosted.org/packages/f8/02/ae940ae1127635aecfb9666e0a0c5a07a8b2a6e96b0a2100c50911448bd7/pikepdf-10.7.2-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:d57eb9455cdbf24492784bce17b7b1ab7e0d0443d3e3392564448cf57aae9ac2", size = 4687566, upload-time = "2026-05-25T07:49:39.357Z" }, - { url = "https://files.pythonhosted.org/packages/73/f2/6a88ba03334a2767ddfd774bb4045c28ca73af795695ca75eb1c0ceb3797/pikepdf-10.7.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2deb9988eb2a53ef13ca3c089456532e96411bf37ffe465918db5941e8467c68", size = 1942949, upload-time = "2026-05-25T07:49:41.218Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7a/e61f342b1dcffacabdb28356d87e8f9e466d5c088de7eb6a9e21ea5d6c39/pikepdf-10.7.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a830905e52401b70c0da6152fe39546551e0b81f60b000caee7661ad9b10ea6e", size = 2133244, upload-time = "2026-05-25T07:49:43.234Z" }, - { url = "https://files.pythonhosted.org/packages/72/88/11d4f4ada0e36efe05f1b85511031e8fc6fc0cb8f289e3e78eb55f1ccf11/pikepdf-10.7.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21c7299baab8d968f3dbfd425fd7a5f58ac593f9615722c9ba344b267ebb3c25", size = 3582062, upload-time = "2026-05-25T07:49:44.886Z" }, - { url = "https://files.pythonhosted.org/packages/34/40/2000f0a4ec3b2cba33896df36e724a9a5de41c2298a198dc7e619d2480c0/pikepdf-10.7.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:19a78d8ed211839151eda00df7b06eb23036499218bf0604d1b498879b02a1a8", size = 3777832, upload-time = "2026-05-25T07:49:46.605Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a7/689a653723ce367e050d2c86651d46e8673da1c57302345c3801ecc5f28b/pikepdf-10.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:9be1590b598a27561c783dc5e7268fea7c5b9ea287e7da48433286dabb514b6c", size = 3243951, upload-time = "2026-05-25T07:49:48.792Z" }, - { url = "https://files.pythonhosted.org/packages/9d/80/00c2e1f0b17f585c3d5edbd87a10450d7b01e84be6ff28a718a7f7513992/pikepdf-10.7.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:d680da5630ede2ed7170ad7135bcf02002dedb391b2902d5820c0fa01f9a4f5a", size = 4370990, upload-time = "2026-05-25T07:49:50.954Z" }, - { url = "https://files.pythonhosted.org/packages/c2/1f/eaca736fe7bc2b2e983e0755cb955218ce913f858c5ca6aeb91597406aa1/pikepdf-10.7.2-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:b20e41bc30bc663035d34e9b980dd72d98514f2685bf71566f3f22d63df3259a", size = 4687963, upload-time = "2026-05-25T07:49:53.129Z" }, - { url = "https://files.pythonhosted.org/packages/27/1e/9bbfed92cbbc2c9ddbb81e9db5860778d1692c298b6c410cc7fd4abdd6e8/pikepdf-10.7.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d40627583d24379735136eea49cf6d357452c6b96119111fed95bc266fd62311", size = 1943293, upload-time = "2026-05-25T07:49:55.3Z" }, - { url = "https://files.pythonhosted.org/packages/28/59/67a0640966da5771ba0dd7a9a4b77c7781e5912718849425982ab3d250ae/pikepdf-10.7.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34d446e792f762e13bb2f1133061fa08e7c0378a3d095ca75289c9183dbd1fd7", size = 2133477, upload-time = "2026-05-25T07:49:57.103Z" }, - { url = "https://files.pythonhosted.org/packages/fe/63/6f467b1e4408baf021b3493ffeee4b848b7dd965a82f831ce199352ed0c2/pikepdf-10.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2b97e7dd90ea882b00e5c3b2250c6f81dfa1d5efefe7f3bf8cc5902f7e00c5f3", size = 3582278, upload-time = "2026-05-25T07:49:59.149Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9a/46817303890dfa7d1456c4ec6ef4ffd6947fbc3314c960d3d473fa38c916/pikepdf-10.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:125c1238ff040b18885caa0cdfb13f3fe328fc04d72d8eddccddce248586201f", size = 3778138, upload-time = "2026-05-25T07:50:01.217Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ad/b18e5fdac925cbfbfe5fa8b6bf0cfa83bc92184a20b98a5a4294e444d33f/pikepdf-10.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:88cd03c5855d9d185c202da5ac80fbcec43b11fb98b26d05a709a95163472b22", size = 3244077, upload-time = "2026-05-25T07:50:04.109Z" }, - { url = "https://files.pythonhosted.org/packages/14/d3/7420c9e9d82fd3f689c073d93f4b942fe9d4c944afb20eea9cb55e039796/pikepdf-10.7.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7713fc65494c4480ce12e55801dc6750ed64805ce929a970e8cc6f234ac87abb", size = 4370281, upload-time = "2026-05-25T07:50:06.327Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b9/163b23f333868044336d5009268d707fa314a432bf72a25acc334083e63a/pikepdf-10.7.2-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:4ff1b21ab8e148c091747f2ebe62d39712882f91f67ae916bec3d6c97a09879d", size = 4689384, upload-time = "2026-05-25T07:50:08.258Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/3ee4eb10c1c449766126c60a1f4963ba89722bb30fa0421c02636e0570ab/pikepdf-10.7.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fafe1169738b5ed1f8707435136a419663071e6739a8a75dd298e4a1654497d1", size = 1943573, upload-time = "2026-05-25T07:50:10.158Z" }, - { url = "https://files.pythonhosted.org/packages/fd/46/f55fe215a5e45f4e267d3bf8f2a2a093a1c7b1e1ffdfe12a411a08d71421/pikepdf-10.7.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6257a2677fec7a3d4808c6e957bf07fe06633b8f887b4e02b97a6b476fc1bca6", size = 2133229, upload-time = "2026-05-25T07:50:11.867Z" }, - { url = "https://files.pythonhosted.org/packages/41/99/6922d04de6a74123671a543b5ba4d3b514f7839158822677e975493b8827/pikepdf-10.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cdc116d4acee2a0c2c68991c38d979b6662bef68e2ddd4fda10b362c22ecf331", size = 3582230, upload-time = "2026-05-25T07:50:13.748Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d1/397dd516c3bcf22d01ce75f9a98b4dc549f1cddf6dad34ce7391a30b68d2/pikepdf-10.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6fb6c81cbd69527f834ff44fbecd714dac718b27180f53ac40485c26eb1c871", size = 3777901, upload-time = "2026-05-25T07:50:16.027Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f0/52ebf28ec6321648185754f31d15998bcdc0dc85dc527fa7e1b392c08fdb/pikepdf-10.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:7330ca7831e23fe64f7d6155a7e64f9794145f82b90494cdd0c4e802ccc1d747", size = 3245271, upload-time = "2026-05-25T07:50:18.007Z" }, - { url = "https://files.pythonhosted.org/packages/fb/97/1a00609153977302d24d7949ebf3963ded8255ba880c2b89f0fff001b4f5/pikepdf-10.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9a31f07f97bb72ab8131650d1f150af8383d989e0957233b06a036c04ddf62da", size = 4370127, upload-time = "2026-05-25T07:50:19.964Z" }, - { url = "https://files.pythonhosted.org/packages/8b/98/f736c7e1d7901f29666f9fa12bb68bc687b666a323df1759b5f08bb9f067/pikepdf-10.7.2-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:eda46a05ec887f32a5a7c1e14b5bec961bb2de5f49f56c9c6408f25ae632a1b3", size = 4689336, upload-time = "2026-05-25T07:50:22.038Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f7/07a7d1db5268d989a47f559272d0df08ba933e6d893cf62617d04edd21a4/pikepdf-10.7.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:344fa1bce6720ffc678cb5358582067c0f6cf31d2c93ef52bd248796b0ebddde", size = 1943124, upload-time = "2026-05-25T07:50:23.822Z" }, - { url = "https://files.pythonhosted.org/packages/33/17/2ee02c3e2bd34eadf67c53e458fa342803e60462b193fb774adaf46e06fb/pikepdf-10.7.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:400b73690efa0cee02655ad2be1d6bd01d4f7e16385dbbb9d0dcebc79be13642", size = 2133126, upload-time = "2026-05-25T07:50:25.53Z" }, - { url = "https://files.pythonhosted.org/packages/67/89/3a533d32e87eaca2a578d2ef228eb7c28093da73d6cc6f8ee543727fea93/pikepdf-10.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7b10e0b3a6e7569ae9401fff13f19ece202e3b86303efb9890aa57efcfd44ae6", size = 3581861, upload-time = "2026-05-25T07:50:27.529Z" }, - { url = "https://files.pythonhosted.org/packages/21/73/1aee451e7bad3df04c55b7d42c96c9664d1ffd914eae33a8100273244310/pikepdf-10.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5662f665bd64a319a3f41b19b86aca3df284fc34fdf1f35fd58c546c459f4459", size = 3777914, upload-time = "2026-05-25T07:50:29.626Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f4/0a790da4860da6302477792f2e1ae419ca5681ae1dfca2cd8aef8c909c42/pikepdf-10.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:fd26fdd177c388b3476108d259164d849cfbedd4ae4f4755037cca3f79f2c31f", size = 3245245, upload-time = "2026-05-25T07:50:31.524Z" }, - { url = "https://files.pythonhosted.org/packages/ee/d6/ae1e6e654ac49d2e764266ab9a08c584e33a3b231978b01435d7a1818464/pikepdf-10.7.2-cp314-abi3-macosx_14_0_arm64.whl", hash = "sha256:4ce80c306b5b146f812c12c04bb2c1ee40d7ffd3e4f767dd2879c7db4df547ac", size = 4373082, upload-time = "2026-05-25T07:50:33.398Z" }, - { url = "https://files.pythonhosted.org/packages/ba/2f/1f59cf70d94ba000e6be61ec756f69a2e1b80e99d4b09bbc5a00c076f7cc/pikepdf-10.7.2-cp314-abi3-macosx_15_0_x86_64.whl", hash = "sha256:7bccecac292b5f47cfbdcd5865fbe8f7ece7276999a66f5ea3b9489ab7332cd9", size = 4692049, upload-time = "2026-05-25T07:50:35.692Z" }, - { url = "https://files.pythonhosted.org/packages/62/35/b798cd50a3b1f1c745b002d6d1752211077d0676692566882e53e5fb5649/pikepdf-10.7.2-cp314-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be33064c06c46a9063012a73eb9c54a0bf0f82e7acfcfdf7fbb7755b0167ab7e", size = 1941061, upload-time = "2026-05-25T07:50:37.577Z" }, - { url = "https://files.pythonhosted.org/packages/04/d0/3d9ed90854f4c9e1b22c6db71f5f1a32d196734632348ddadc069473fffe/pikepdf-10.7.2-cp314-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7c69af4e331dbd2495de700e84ef26f8b7178e010ff02f121b3392dfbaf968", size = 2130570, upload-time = "2026-05-25T07:50:39.432Z" }, - { url = "https://files.pythonhosted.org/packages/66/ea/49652be58b8551417a6af67b3161d6b20f83cac1aa1fc05ec1fcf51181d1/pikepdf-10.7.2-cp314-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:97f8867140f83b12cfa546b2f5b8a430fe7dcfe614964b1b5f8e75774edcb898", size = 3579364, upload-time = "2026-05-25T07:50:41.691Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/3e1073eae42cc2e8da76cd98f94a252f7c848b2997f90dd1294f5eb78143/pikepdf-10.7.2-cp314-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c3413e69cd2cc90e978c23aa3bb7594ca51d2316a25593f97cad3a31fddd6402", size = 3775551, upload-time = "2026-05-25T07:50:44.101Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/10847460a00c6454d5a11ecfd8f62afeb9dab70aad85eeff54be3b307bfe/pikepdf-10.7.2-cp314-abi3-win_amd64.whl", hash = "sha256:cba013f6164f65d9d932bcf15847782fed2507ff8504ff3b75267f7ff3a8cfa0", size = 3244759, upload-time = "2026-05-25T07:50:46.454Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/a796e796e1a83e7e75b0ee061fbbe3b52889305d79ddf495a557e365ad22/pikepdf-10.9.1.tar.gz", hash = "sha256:410fcf32bc9c8a0a96d94bbd6268ba7585333b1423b93a5fa2ef3c05f4eba3da", size = 23854194, upload-time = "2026-06-19T23:53:53.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/bb/aba50d320954e64a8f263c9f083001c0a7493c5b6e4fb5287708327aad0d/pikepdf-10.9.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:86423448997d7e51007838eb5c4feb644be0ea17693e2e0b5e0a4dee38c64a04", size = 4429411, upload-time = "2026-06-19T23:52:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9f/0c4a420883508b532e836e4c6f9f61dc3270c8215ff473d3c639b0d883da/pikepdf-10.9.1-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:c1da65280a9ea5d1ee6ec102ff08f80ca7c6d78cdaae62b1bbec901acfafa816", size = 4748081, upload-time = "2026-06-19T23:52:22.513Z" }, + { url = "https://files.pythonhosted.org/packages/83/96/5a5759497eea71996005a182f7636a8570567c297ef43c39948aa4a5b867/pikepdf-10.9.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7468f70f760a59c261a4980698d91a7e5aa3cd77ee9550fc7b44255406d033b5", size = 2002971, upload-time = "2026-06-19T23:52:24.95Z" }, + { url = "https://files.pythonhosted.org/packages/23/2a/9c43fde3a90367a80bcb18900d935ad3e934d906787211d534d8419be1f0/pikepdf-10.9.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92d58fdea788379bfe06be3bfd64a4a5cac346f8696e7564622947362194a48d", size = 2194855, upload-time = "2026-06-19T23:52:26.846Z" }, + { url = "https://files.pythonhosted.org/packages/46/45/566f2988a69c4c5d3fd18c10bfd05340b7a730f440c1bd746dc077682068/pikepdf-10.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:86e02c09ddc7c34f04caebd6d012b97f1b1195b52f56074411a604585d7804dd", size = 3641022, upload-time = "2026-06-19T23:52:28.773Z" }, + { url = "https://files.pythonhosted.org/packages/54/05/5c7ae69c271eb569a030f71b72059a17439c040653b13f8973eb277e4223/pikepdf-10.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b968e54f723abef90d38a8b0c50dfb5f763de594f7d5aed6317d015a40d985de", size = 3838097, upload-time = "2026-06-19T23:52:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/72/4a/da3cb5e752613dad6cb1463f8bc738b637c0536ce64de27e71467d571d01/pikepdf-10.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:e280d58399847771b05f9d3c8a88c4f0d5756d231aadbc9bcb4325d55c45fc41", size = 3295934, upload-time = "2026-06-19T23:52:33.149Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a1/39fb00f493e761bc7e03a6ee68f71f86d93f31adcbb8cea9294890072e45/pikepdf-10.9.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ae9d82222c30559dfb2b17df873d2b80d5f4f9c7e7522e1d91b8d5bf47d0a8b1", size = 4429370, upload-time = "2026-06-19T23:52:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c1/d015bca207a6749e6f2a52a2829910430cfad8ee3e18673ca98d5059de8b/pikepdf-10.9.1-cp311-cp311-macosx_15_0_x86_64.whl", hash = "sha256:edb908bb62c18e9d3a307f3cbeab5f3b7cf063806b947a00f3baea7ad0c686c0", size = 4748104, upload-time = "2026-06-19T23:52:37.446Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1b/453436f41a43a890c4d0a79c96b97589def9cce832d90b512e87a5bfdb7a/pikepdf-10.9.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a950ed52c312931b8c6523953f7358efeac62d46dbd17a776338a1e7a77d0627", size = 2002859, upload-time = "2026-06-19T23:52:39.867Z" }, + { url = "https://files.pythonhosted.org/packages/87/eb/e926add2b6e269d14ad56213a1217fc4bfa3647c0262e341a13349e5f3fe/pikepdf-10.9.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1e6f3475bd142a72dc971cac5ba14190878ebc0435d5222264d4a681cbda569", size = 2194914, upload-time = "2026-06-19T23:52:41.595Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3d/1328a9910dbe5f85d492527aad40d03f36b5f96c7d2317ed98dab48d88f0/pikepdf-10.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba4f4550132ce3e8fa2711f4e325a1579ba3031dc977580323f0a404ec9fb695", size = 3640944, upload-time = "2026-06-19T23:52:43.612Z" }, + { url = "https://files.pythonhosted.org/packages/c0/58/ef5767cc8bfaced10dcdda3327c89130d26e236574ad4a5797e77fd9ada6/pikepdf-10.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3cdb758623b450172240e42270072981c1821f236a023a29adda3ce6baf9d664", size = 3837494, upload-time = "2026-06-19T23:52:45.724Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a3/ecf64e38fa95e39a36f012ea22554549d0d8abe916cbb27a394191ceef44/pikepdf-10.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:86ca561cb9c9130dea9cb54d3144db866f439e509dbbe285eba507811a8563d2", size = 3295876, upload-time = "2026-06-19T23:52:48.235Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b3/61b86a9e11a85ea579ca3d9465e6ddfea79f4baf6be17f7b244569b9bcdb/pikepdf-10.9.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cdc7520523da4966c7afabed20b6c57bc87de74e5e21d0e193b010495fb8f881", size = 4428335, upload-time = "2026-06-19T23:52:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/ca/7a/01679a404d198170785211b6dd32fd5cab25f3f952e5dececd8e1525b4a6/pikepdf-10.9.1-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:08d2bee9fd5d7a32530cc2b0bd167600a0a296e4445823469b98f20907881bd6", size = 4748362, upload-time = "2026-06-19T23:52:52.73Z" }, + { url = "https://files.pythonhosted.org/packages/ee/46/62ecb0480f4c1002d2183609a27df290301e2161fd062152e94bb281d522/pikepdf-10.9.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea24f7f1756a7832fc7e27c9c4e1d7c98c1b847c00c3833299c5c03cf15399c", size = 2000233, upload-time = "2026-06-19T23:52:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e1/6683d947603398d4db11e6ee4fdf2323f3fd7ffa3c6ad06995f213e2b736/pikepdf-10.9.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3836fc0547af990a2b8a37cda63526ef095dd36b4599cf28cda070a9994241ef", size = 2192740, upload-time = "2026-06-19T23:52:56.947Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/3489d4cf70023528e2d5968a61be7f01349ea311cf25cbd7e7d2ccf1bdb7/pikepdf-10.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b9d26aeb4a5ba4bbaf03102906ef3a2333aa764785b98cc2769238635be7b4f8", size = 3638408, upload-time = "2026-06-19T23:52:59.074Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8d/df1b6f6aa9cad37d6d5545a5078b7347a60e07066fd56d267cc4ef2ba9c9/pikepdf-10.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84d2ffeac1bd20612db7346ea7b200c78d2d767cbbd6bb52a47d6e1153fa3933", size = 3836337, upload-time = "2026-06-19T23:53:01.408Z" }, + { url = "https://files.pythonhosted.org/packages/20/68/734695c9be596354e2f38e77aa1be42a919a6db669c802810e662c17df62/pikepdf-10.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:8dee389db583673a4c37036960f2980085aafc8f5b55f5843af23cd061f45fb9", size = 3296420, upload-time = "2026-06-19T23:53:03.265Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0e/f99fc15ce0150455c1203e1abe68148d13da9a6f605d94e2a499d1194bed/pikepdf-10.9.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d9ce2812bd76ef6f3549546a4ad33c81be7e64a9985997628d084e8863d157d2", size = 4428220, upload-time = "2026-06-19T23:53:05.648Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2f/34deea50e306095533a8420ff4d6dd90d8c9c72ad9b46977dae63cbc4ae4/pikepdf-10.9.1-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:15880e09605f0c2f8afeacffb7c3b904e01569922596cc10ee3afc3777406206", size = 4748238, upload-time = "2026-06-19T23:53:07.852Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c1/f0c60cfde4116e8b01687f74c1792567c40a9bf3e1b3ee5ceab7267fedde/pikepdf-10.9.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84dfb8c7082d85106d291bec71990f03215215a07852a698bd5d84337ce31949", size = 2000169, upload-time = "2026-06-19T23:53:09.91Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8e/533188d63f2b14bb6103bed6e048f2d5676622e2f902a3dff558ba4b40be/pikepdf-10.9.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16b7edf32814256b2e50d31336a0301371a397c466bf7624bce7639e66c07b97", size = 2192691, upload-time = "2026-06-19T23:53:11.756Z" }, + { url = "https://files.pythonhosted.org/packages/97/40/317206653f2afbfc89ecf5e896bdce4ce26e11a78c87ca51a49bef73be29/pikepdf-10.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3311c44a8553f2c57bce37cc764df2a9c07ecc43a39690f7fc10c7fe081083e", size = 3638153, upload-time = "2026-06-19T23:53:13.692Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d3/cfdb8207c3e6906853f04d857eb9dbc6cc98624d17481360c0b7cca2d4db/pikepdf-10.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b7a5ff6cab6b2aed3058e362ec64d41f0c03e842eeecf9a5af30e21988ad6de0", size = 3836156, upload-time = "2026-06-19T23:53:16.093Z" }, + { url = "https://files.pythonhosted.org/packages/28/9c/4a7a588c748431b91dc617cd3dbbb79387d8a10fecc8a363151176df450e/pikepdf-10.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:6fc3096e88d8f030252a43c44c808a89c2c9905988f0d66b72b5731f8a9e3a6e", size = 3296374, upload-time = "2026-06-19T23:53:18.635Z" }, + { url = "https://files.pythonhosted.org/packages/36/a8/551a73c191c8742fd0436051ed542b245aeea517eb7ee2d91215b9a4047e/pikepdf-10.9.1-cp314-abi3-macosx_14_0_arm64.whl", hash = "sha256:3f9c79dc03ee8a44b8c4265baced60b8dd2c79bfa447a22eec67f9deade5d535", size = 4428390, upload-time = "2026-06-19T23:53:20.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c5/6b6d0d204bd51517e4240a349e5b3c1d83aca096aa6b115878385ed7e67c/pikepdf-10.9.1-cp314-abi3-macosx_15_0_x86_64.whl", hash = "sha256:cfb8146c396d4d8a92f20c72df0ec94a2ef61770cece29d27821b0fa2f52ce1b", size = 4748541, upload-time = "2026-06-19T23:53:23.153Z" }, + { url = "https://files.pythonhosted.org/packages/84/30/e4d13bbdbe726be9546e8ce6d81f16a650f9786e9fe8d3ba9eb8d4055455/pikepdf-10.9.1-cp314-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbeae48b65b8e9ed28419ebdd52b9cec7bf60a52c984d311b6df2458c3eee62", size = 1997375, upload-time = "2026-06-19T23:53:25.871Z" }, + { url = "https://files.pythonhosted.org/packages/cd/64/b214a90fcfbb653d8ec5c33501028d163a3ecf51e267a72f0c5c363d328d/pikepdf-10.9.1-cp314-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8d19462732f782d1ddd1c556fec5195453b3fc758f4d4c83d7294700cc496e0", size = 2190496, upload-time = "2026-06-19T23:53:27.996Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4c/627c73edf668113f837faa603cfba4140ed2836981547025abb8fc238526/pikepdf-10.9.1-cp314-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c85627cd6122d5cc734069793ebfdbeccd60679cbd2c241bc7a199797cf802d", size = 3635409, upload-time = "2026-06-19T23:53:30.268Z" }, + { url = "https://files.pythonhosted.org/packages/8a/16/83df0c82a2ee31e7b47e1cecbe8ef9de66b55642833f308bde559dd469b0/pikepdf-10.9.1-cp314-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d1d0c635368b1cd14e88e6a5a007c5271c30d1798d88452de0e88e4d6ee92335", size = 3833699, upload-time = "2026-06-19T23:53:32.46Z" }, + { url = "https://files.pythonhosted.org/packages/ec/75/4bc9beb1de126f4db1b6ac807852e3db212ba884e94b85c7552fc5735204/pikepdf-10.9.1-cp314-abi3-win_amd64.whl", hash = "sha256:cd6c12ed7a1d8327b1279d8d14890aedc10090be38114f5a0893026f217c06aa", size = 3389178, upload-time = "2026-06-19T23:53:34.67Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2bec744893aa2843af18a9bb03475dc193b9ed392ec49420946cd3c441e9/pikepdf-10.9.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c748df16d09742e34ea3dd7e44ad64f88331d2cddf62d4d73911bc4193268f62", size = 4434676, upload-time = "2026-06-19T23:53:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f2/a9b6a46b6610ed20484e1a245a4039738359448b471855de352074c972d3/pikepdf-10.9.1-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:99d1f8fe53fa5c9d78a8c7c2ea4a7aad228e1b7b0571287de7f9decf0b5561c1", size = 4753523, upload-time = "2026-06-19T23:53:39.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/53f7a73ad0344387254c938aa39b359a9b9979341c2dd14a7348e1ecab40/pikepdf-10.9.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee509c16b86e007d205df2083b1012ec75c825fd62fdd90aca292fd4c7a4b213", size = 2004802, upload-time = "2026-06-19T23:53:41.876Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/13f41886d7e73fbe9056fae291a4d883fff9f37879c98b8ca36f89b1bfe5/pikepdf-10.9.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91756746333a30ef286b8d3ead7a1382f6abb067519f3841c4a6392773059309", size = 2196628, upload-time = "2026-06-19T23:53:43.938Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f1/a8f44aa54d766a547160e29b0f3358ad1d6bc7c4b1bdfac097a470ad544a/pikepdf-10.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:06584062796a2bdc201cb76c1452a2651240c7140102ffb00cde4e4808b84a21", size = 3644233, upload-time = "2026-06-19T23:53:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1e/5a1493dc73b82f3721ec75dc0abeb8d357b8dee6a1abd6edc8ca75d874c1/pikepdf-10.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8534f300fe4d1fcf311aa6ea073284d414e4a9c0564c38c0ef020f14825fb857", size = 3840741, upload-time = "2026-06-19T23:53:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/5a/03/6a736a6d4be7e45c0b662f9489a519b6298f91621afa78827a5459ee5983/pikepdf-10.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8e470a1470a809da83643c30e2e05ba629131b0193d32bc91f28c7b18cb8b23f", size = 3420520, upload-time = "2026-06-19T23:53:50.276Z" }, ] [[package]] name = "pillow" -version = "12.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, - { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, - { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, - { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +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]] @@ -1937,7 +1955,7 @@ wheels = [ [[package]] name = "py7zr" -version = "1.1.0" +version = "1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-zstd", marker = "python_full_version < '3.14'" }, @@ -1951,9 +1969,9 @@ dependencies = [ { name = "pyppmd" }, { name = "texttable" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/e6/01fb15361ca75ee5d01df6361825a49816a836c99980c5481da0e40c6877/py7zr-1.1.0.tar.gz", hash = "sha256:087b1a94861ad9eb4d21604f6aaa0a8986a7e00580abd79fedd6f82fecf0592c", size = 70855, upload-time = "2025-12-21T03:27:44.653Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/3f/ac248f25f3901d3d6cf80ac99d5bac6fd1e5e6543c1812c79b02b6074c60/py7zr-1.1.3.tar.gz", hash = "sha256:8d51894abb38355bf14881088bb97f01fe4cb5b14ebe22f66d6297668c7e1a74", size = 71695, upload-time = "2026-06-19T09:28:29.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/9c/762284710ead9076eeecd55fb60509c19cd1f4bea811df5f3603725b44cb/py7zr-1.1.0-py3-none-any.whl", hash = "sha256:5921bc30fb72b5453aafe3b2183664c08ef508cde2655988d5e9bd6078353ef7", size = 71257, upload-time = "2025-12-21T03:27:42.881Z" }, + { url = "https://files.pythonhosted.org/packages/88/40/dc7f804abd0ea167ea8d4d9b3f5455321aaf671d040fcef647e9e952e714/py7zr-1.1.3-py3-none-any.whl", hash = "sha256:17934a35089e026dec6c72ee275d9b841e646881ef822d618e805c3006661d9a", size = 72242, upload-time = "2026-06-19T09:28:27.512Z" }, ] [[package]] @@ -2082,15 +2100,15 @@ wheels = [ [[package]] name = "pymdown-extensions" -version = "10.21.3" +version = "11.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] [[package]] @@ -2189,7 +2207,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2200,9 +2218,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -2233,18 +2251,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] -[[package]] -name = "pytest-gitignore" -version = "1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1a/3e/4732f19116915674dbcf636c043c4d9e6585b3ee3c46f04555c4e437cd9b/pytest-gitignore-1.3.tar.gz", hash = "sha256:e62a59ad42731504926a0f7b66b0dd829b587ac9c07e304834ae050c82aca1e3", size = 2547, upload-time = "2015-07-17T14:50:17.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/3e/628dc065ae01b768951c332a676857ee7f8736a195158e69528ac461e9cd/pytest_gitignore-1.3-py2.py3-none-any.whl", hash = "sha256:a7ed6d47c3aa10807ecb76c5b52716dce512bc704d68717b238c65f6b59b6cb1", size = 3640, upload-time = "2015-07-17T14:50:21.295Z" }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2362,123 +2368,123 @@ wheels = [ [[package]] name = "regex" -version = "2026.5.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ed/0ad2c8edf634918eb4484365d3819fa7bd7f58daf807fe7fb21812c316e5/regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44", size = 489438, upload-time = "2026-05-09T23:11:29.374Z" }, - { url = "https://files.pythonhosted.org/packages/89/a9/4ed972ad263963b860b7c3e86e0e1bcc791def47b43b8c8efe57e710f139/regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a", size = 291270, upload-time = "2026-05-09T23:11:33.254Z" }, - { url = "https://files.pythonhosted.org/packages/16/81/075930d9fa28c4ea1f53398dd015ee7c882f623539759113cda1257f4b82/regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733", size = 289198, upload-time = "2026-05-09T23:11:35.769Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c8/5cdfbf0b5dc6599e1b6131eff43262e5275d4ec3469ce10216061659aadb/regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2", size = 784765, upload-time = "2026-05-09T23:11:37.689Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ca/ae5fd6edc59b7f84b904b31d6ec39a860cbcecd10f64bd5a062ca83a4864/regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea", size = 852115, upload-time = "2026-05-09T23:11:39.973Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ce/a91cf555afb51f3b74a182e24ba073b91ea7bb64592fc4b315c111bb19fd/regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538", size = 899503, upload-time = "2026-05-09T23:11:42.48Z" }, - { url = "https://files.pythonhosted.org/packages/55/7f/725a0a2b245a4cf0c4bab29d0e97c74285d94136a65d1b55a6459a583502/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2", size = 794093, upload-time = "2026-05-09T23:11:44.681Z" }, - { url = "https://files.pythonhosted.org/packages/e3/2a/996efbd59ce6b5d4a09e3af6180ceb62af171f4a9a6fb557d2f0ae0d462b/regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989", size = 786234, upload-time = "2026-05-09T23:11:46.882Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/8731e8b8806174c9cdd5903f80a14990331c1f42fc4209b540952e9e010d/regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9", size = 769895, upload-time = "2026-05-09T23:11:49.324Z" }, - { url = "https://files.pythonhosted.org/packages/9a/0b/932473194bd563f342a412ae2ffbbd6da608306a2bc4e99249a41c2b0b92/regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00", size = 774991, upload-time = "2026-05-09T23:11:51.261Z" }, - { url = "https://files.pythonhosted.org/packages/98/80/9523d196010031df25f7177ee0a467efbee436324038e5d99def17a57515/regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808", size = 848790, upload-time = "2026-05-09T23:11:53.232Z" }, - { url = "https://files.pythonhosted.org/packages/3c/07/56987b35e89edf47e4a38cf2845aeee476bfa688a6bdbd3e820cda461dc1/regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248", size = 757679, upload-time = "2026-05-09T23:11:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/04/2a/ff713fff0c566507c06a4ce2dc0ae8e7eeebc88811a95fc81cf1e7d534dd/regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6", size = 837116, upload-time = "2026-05-09T23:11:57.934Z" }, - { url = "https://files.pythonhosted.org/packages/77/90/df6d982b03e3614785c6937ba51b57f6733d97d2ee1c9bc7531dbfab3a54/regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4", size = 782081, upload-time = "2026-05-09T23:11:59.607Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/4e88a5f7c3e98489aac4dd23142723d907b2a595b4a6abcbacabefeded09/regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac", size = 266247, upload-time = "2026-05-09T23:12:01.116Z" }, - { url = "https://files.pythonhosted.org/packages/6a/40/4b224cb0582b2dca1786726e6cdabe26abbf757d7f6718332f186da155d2/regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03", size = 278416, upload-time = "2026-05-09T23:12:03.2Z" }, - { url = "https://files.pythonhosted.org/packages/12/4d/014fbe803204cab0947ee428f09f658a29632053dde1d3c6176bb4f0fd4c/regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b", size = 270413, upload-time = "2026-05-09T23:12:04.649Z" }, - { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" }, - { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" }, - { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" }, - { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" }, - { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" }, - { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" }, - { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" }, - { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" }, - { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" }, - { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" }, - { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, - { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, - { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, - { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, - { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, - { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, - { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, - { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, - { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, - { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, - { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, - { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, - { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, - { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, - { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, - { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, - { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, - { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, - { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, - { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, - { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, - { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, - { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, - { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, - { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, - { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, - { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, - { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, - { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, - { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, - { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, - { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, - { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, - { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, - { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, - { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, - { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, - { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, - { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, - { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, - { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, +version = "2026.6.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/05/e4f219230e11e774a6c9987d2ab0d0c6b8573e13a17e143d0015bee710ef/regex-2026.6.28.tar.gz", hash = "sha256:3cb4b6c5cb3060cc31efdc1fbb27c25fb9b29044afd87e40601a1c4d9db54342", size = 416101, upload-time = "2026-06-28T19:56:55.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/dc/f7a8c9cf0768f704153d358fae2bc883199bc4ea1e4aa458f1be9d0ef2ce/regex-2026.6.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b83932645630965fd860fdb70ebbf964bf3e8007f08851ea424d01f8d35454a8", size = 489471, upload-time = "2026-06-28T19:53:06.385Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/9786a4a2133e2f1cc5897ed3d2da3da29ff54b775ffa38bc5935fc24be82/regex-2026.6.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e81f1952355042e517dc9861ce65c676e4a098f42402993c40461786d1f794d4", size = 291294, upload-time = "2026-06-28T19:53:09.232Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1f/bfe5b529257f0853aa6b94146e0f6462f4d45aa4f3c05d5a828f415dfd40/regex-2026.6.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2097591101d70bcc108af64c46f6066bb698ee067fec5f75beac0be317639311", size = 289216, upload-time = "2026-06-28T19:53:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/25/56/f615165e90ac5f3b72b249240643439520bbac0ac60a9de06868528eba4c/regex-2026.6.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d7538a614b5842bf53ce329d07b43f97754ca7e6db8d69f347e071bce1c953", size = 784787, upload-time = "2026-06-28T19:53:12.393Z" }, + { url = "https://files.pythonhosted.org/packages/04/94/c9e3ad31b3d5fbe1228fee8319e0c02a5460296624f220d08764547fe6ae/regex-2026.6.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5561e47bbe2b75373b695326507743fcdd4d2cc7f5022312024ccf39fa094e0", size = 852137, upload-time = "2026-06-28T19:53:14.287Z" }, + { url = "https://files.pythonhosted.org/packages/c0/77/d506a428e446466ee298f5425a774737d0671d070425ed794bb3314d60c6/regex-2026.6.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c10f2c5a55ab3dd8318d8ad5f11b530e2691c0edebebde7713066f484902c3fb", size = 899525, upload-time = "2026-06-28T19:53:15.987Z" }, + { url = "https://files.pythonhosted.org/packages/aa/72/becc00d839f19401f10a20168b44711c7b02f7f62bba875b2d8f98417435/regex-2026.6.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23f7e0cc60c72486b42a685f1ff4eec90d50d4fb05e4f9c7d5363b03aa02600d", size = 794116, upload-time = "2026-06-28T19:53:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/fa/11/ea2ca423eeaac2e18077a18b058614e9201f130750df2126d444e39acab2/regex-2026.6.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:76493755f79a88d5ed2c9e63a41d3c05997e0a7ffbe76ed8c4ded8be35b8b14c", size = 786257, upload-time = "2026-06-28T19:53:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9e/f5bf7ecbd14ff2086f015c54dc24fd0d74ba5327fef0de479213f8128615/regex-2026.6.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ff0f41a00f23ea5054acb61901380c41813d813eee3f80f800995710bcc52ecd", size = 769914, upload-time = "2026-06-28T19:53:20.564Z" }, + { url = "https://files.pythonhosted.org/packages/43/04/f9040a5360a06241ba5b7f2e6f1c6184e104a84e6f6522535700e94bf8e2/regex-2026.6.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c60b297292e7e1ef5d02a4759f9e452ee4c8bb95e168d8fd0b5db01bd806f9f", size = 775013, upload-time = "2026-06-28T19:53:22.067Z" }, + { url = "https://files.pythonhosted.org/packages/73/97/4e46f7abf2f864319d2bcac609af3c0532968c66a3364337778fd232b83c/regex-2026.6.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7cf03c87f7b9cbc25a8894cf9be83818406677b6b391b003ec7c884923387b5", size = 848814, upload-time = "2026-06-28T19:53:24.575Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b8/3d1f995727799a1e2e693e397acb7358094606e5591b6b5fd3128d2d1409/regex-2026.6.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:418208ea0af51cfed4f46eb9b1ea7cfc990ca284f0084ecbd951460fb089421e", size = 757702, upload-time = "2026-06-28T19:53:26.215Z" }, + { url = "https://files.pythonhosted.org/packages/20/10/fd5653b8572910a4fe9055f8959b070d7d9443c94ce986529fcdb5fb2a3c/regex-2026.6.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7635fa2cddb917a6bbfac7890602573d2d8c4e470703b0640e6f86a988817ec3", size = 837140, upload-time = "2026-06-28T19:53:27.655Z" }, + { url = "https://files.pythonhosted.org/packages/5d/31/da77e3ef7b594a2aacbd03ce3d0050f33ab3e021df50c6901467c9006511/regex-2026.6.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7bb96c13d6cf5880d31bbef84ca701a64d738aa491c2b79975cc33f8ad00a31e", size = 782105, upload-time = "2026-06-28T19:53:29.375Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/c379001448d0f58b6946f168d4af96ad60a16c1553259c27b0df8701b640/regex-2026.6.28-cp310-cp310-win32.whl", hash = "sha256:56f05194c4843957dd8b3af87eb0c52d8cf0509e7f18e172d727f5f8ff840646", size = 266728, upload-time = "2026-06-28T19:53:31.813Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/cb656529efa87d74cce0d69e606c745537016da3bdfae78f342af2242ee3/regex-2026.6.28-cp310-cp310-win_amd64.whl", hash = "sha256:70710927033af3b54369f17aaba1343b97a23d0b1aa994fa1512b08b1b8c136a", size = 277901, upload-time = "2026-06-28T19:53:33.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ac/d35ccc309c9409406445ab2ef0b56f6a341a916ccff49ff9ac5cc6bb8e9b/regex-2026.6.28-cp310-cp310-win_arm64.whl", hash = "sha256:ed7b30185ee3f8b9b053b0be567b4d226016e2afbebc17fde1c6a4580937b688", size = 276880, upload-time = "2026-06-28T19:53:35.029Z" }, + { url = "https://files.pythonhosted.org/packages/72/db/9051b36294bdbabaa9c7db57db0fbcdfbd17f7a106c539bb423d0323faea/regex-2026.6.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a71b51dd08b9b62f055fafab3dee8af8bd2ec81b373a44caef18d6c5ca28f43a", size = 489481, upload-time = "2026-06-28T19:53:36.684Z" }, + { url = "https://files.pythonhosted.org/packages/35/3f/24097a3c3ff30f9a639888900faaecabcf5f54a5bc9c851c297e11b349ef/regex-2026.6.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c26a47770d30a0f85c01e261d2a3ebc342c4af6fd666dbd8c1fe4cbf3adf726", size = 291292, upload-time = "2026-06-28T19:53:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cc/e0d762a189cfb4e8926d16e691720690d139a977b38fdb80230c259332ab/regex-2026.6.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5efbc1af38f97e300d43028e5a92e752d924bcfb7f465d8669d5d5a6e78c233", size = 289232, upload-time = "2026-06-28T19:53:40.181Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c8/ca0ac7f09cc88ca61e0c61c53f7db29334f660ffba5d0b52378e7c44723c/regex-2026.6.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1758df6fdd8c800620a5638958720e8a635e1da49a2f09df2dd63e94a24ec4a", size = 792332, upload-time = "2026-06-28T19:53:41.782Z" }, + { url = "https://files.pythonhosted.org/packages/8e/92/04ae94cbe0dd1f478b2aef6c46f995bb6946d3e338d4b28605478b66a2b7/regex-2026.6.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ad73ecf20c1ef5c975639f8bf845a9370fcf7dada7edc1e3b0bca20e2f8202f6", size = 861743, upload-time = "2026-06-28T19:53:43.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ec/024d7638c807679ff8a0e6081d01d66c7762339af1cac71e45911587ff9a/regex-2026.6.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d80c798b0eec6ea3d45f8816a1e8886c5664615d347d89e8c075b576a1b5a5d", size = 906481, upload-time = "2026-06-28T19:53:44.948Z" }, + { url = "https://files.pythonhosted.org/packages/cd/fd/93bfe5af45f0be4fa8983945455c0e6924e1aeb879cde227958869c1e71c/regex-2026.6.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a361feeaf1b6ba1df060f2ff5c5947092edf537a35ce78e76387ac56d3e0f4a4", size = 799867, upload-time = "2026-06-28T19:53:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/ee/fd/e5d965d41f2398c8ce0f37a4652f03bb297fd009bb796d390134225dda12/regex-2026.6.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b92366d9c8bba9642989534073662abdd9b41faf7603a7ae71597833f3b88f0", size = 773632, upload-time = "2026-06-28T19:53:48.892Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/ff39afaec92b9ee2dba0302a4783976005091681069808938c31cf8df3b6/regex-2026.6.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:11251768cc23f097dd61b18f67966e70f74da822784d17e12a444eb6b29d4288", size = 781669, upload-time = "2026-06-28T19:53:50.693Z" }, + { url = "https://files.pythonhosted.org/packages/45/4e/e2fd4bb8228e10c24af2d7ff867182372190e498eab9fd29cbe54c403c95/regex-2026.6.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad5c67786145ec28a71a267d9f9d92bdc8d70d65541eea852c253f520a01f918", size = 854497, upload-time = "2026-06-28T19:53:52.323Z" }, + { url = "https://files.pythonhosted.org/packages/72/7c/f0340384a973082979064156d05f3d2cc1dced7371efcd7a1b45726a1a8a/regex-2026.6.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f1da438e739765c3e85175ede05816cbede3caaacb1e0680568bda6119bfdfca", size = 763335, upload-time = "2026-06-28T19:53:54.024Z" }, + { url = "https://files.pythonhosted.org/packages/e1/32/90ce0d0898e205506cc22b9c81cfb16b722e06ca5f50fad51c053c2a727b/regex-2026.6.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d98b639046e51c5de64d9f77351532105e99ca271cb6f7640e1f903d6ab63032", size = 844615, upload-time = "2026-06-28T19:53:56.216Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/55abb149599dce1ade687170557129524011eeb3d92afe02429cea7754a2/regex-2026.6.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e164ace4dbab5c6ad4a4ac7c41a2638fe226d0c770a86f2eb041f594bac6ee7", size = 789193, upload-time = "2026-06-28T19:53:57.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ea/cf7f6f6f152e52fdad978b913bf24c14df647eca0f81ef31f3aee0be8982/regex-2026.6.28-cp311-cp311-win32.whl", hash = "sha256:3169a3159e4d99d9ae85ff0ed90ef3b8906cc3152653b6078b842ace6c8f72c3", size = 266731, upload-time = "2026-06-28T19:53:59.938Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cf/a48d8e8d406b22481cad146f48fa0dfca3c5f402b91f26d8e5a0fe4f513d/regex-2026.6.28-cp311-cp311-win_amd64.whl", hash = "sha256:5977295b0a74e8241df8a4b3b27b12412a831f6fa32ee8b755039592cd768c3d", size = 277918, upload-time = "2026-06-28T19:54:01.502Z" }, + { url = "https://files.pythonhosted.org/packages/89/b2/a222392207db7ed86281a732a99f7cf7f2bb35d332799e892b8510be000e/regex-2026.6.28-cp311-cp311-win_arm64.whl", hash = "sha256:f5fbaef40c3e9282ccee4b075f5600a0d858aa0c34147732f1baa69c8188a95d", size = 276876, upload-time = "2026-06-28T19:54:03.411Z" }, + { url = "https://files.pythonhosted.org/packages/da/21/44aa415873032056c43eac21c67285deb2cf66cddb2a964c3cdc8f803efc/regex-2026.6.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:81cc5793ad33a10444445e8d29d3c73e752c8fb2e120772d70fcb6d41df40fe1", size = 490480, upload-time = "2026-06-28T19:54:05.392Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5f/30d4116093c2128099f78b6990dfc1698fdbf3ee528f1e1c647378034c79/regex-2026.6.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e18225243250a1f7d7e5e5d883f3b96465cd79031acf5c6db902b7025f2125d9", size = 292137, upload-time = "2026-06-28T19:54:07.088Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/ca20a0e0de49837e6337603a91ab77556aa27033ac5b975615d98698cfb3/regex-2026.6.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd1638b1c2db1f2d01c182a4b0d3e2e88b0e99910320a745c1727ee3638ddab", size = 289623, upload-time = "2026-06-28T19:54:08.762Z" }, + { url = "https://files.pythonhosted.org/packages/50/11/c013422a7e2c59946df8ac93e792a4922c98287f2a2181341603c78a5d98/regex-2026.6.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4303ebe16b74eeb3fe2715745023266fea92fd44a23f3e7bb2fb48c7a7bbc195", size = 796756, upload-time = "2026-06-28T19:54:10.616Z" }, + { url = "https://files.pythonhosted.org/packages/b0/95/1309645a0e1ee6fb91d954501da57a0b33d50ad2a9acb313702851a7054e/regex-2026.6.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56b856b70b96c381d837f609eee442a1bd320cd2159f5c294b679552fb1a7eaf", size = 865465, upload-time = "2026-06-28T19:54:12.742Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/491802db47c6f5e2904ffa2518ad3ac27fe6bbf5a66d73210a95cc080d47/regex-2026.6.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f74675ab76ab1d005ffba4dee308e53e89efc22be6e9f9fae5b539a3f81bdff2", size = 912350, upload-time = "2026-06-28T19:54:14.508Z" }, + { url = "https://files.pythonhosted.org/packages/5e/60/3ba57840bcc7e2367090360de0c15a5ba6ad22be89314251105f2e943f43/regex-2026.6.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90581684565a93f7258af1e5d3f41ef20d7d7c61f2a428183a342bcb65485e38", size = 801261, upload-time = "2026-06-28T19:54:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/eb/27/af1eb74e9a78c782b3e450b611a595e44906da8a5107e1227f4a7fd0480b/regex-2026.6.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:28f9e6c28f9b90f6f784595a33240a57e181e61b6ee3dc259b25c61e356d1aa3", size = 777072, upload-time = "2026-06-28T19:54:18.128Z" }, + { url = "https://files.pythonhosted.org/packages/20/18/fdd4c883a39e3ed00d669062af1135809bfd3281bf528150849fbd68825b/regex-2026.6.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:378a71d861fc7c8806b04ac5b133d53c0e774f92f5d9663a539872d3fa2b0417", size = 785119, upload-time = "2026-06-28T19:54:20.314Z" }, + { url = "https://files.pythonhosted.org/packages/1c/79/0aabe34b8482dcadf64355f70f96e22eba5ec6c1efb33563f89654f4061c/regex-2026.6.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc199874ecd6267a49b111052250825bfe19b5101b23b2ba80f54efa3e0994e", size = 860118, upload-time = "2026-06-28T19:54:22.368Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2c/c973323306a27c9db7d160e9584eb7e0ece2a96224ccb0d39060558b31f9/regex-2026.6.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b916a10431494ef4b4d62c6c89cab6426af7873125b8cd6c15811bf5fc58eec8", size = 765786, upload-time = "2026-06-28T19:54:24.265Z" }, + { url = "https://files.pythonhosted.org/packages/e3/df/9ca3e378e352242a4cb45573a5e9162c3ee791507702a23966fa559e36b5/regex-2026.6.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e27727fba075f1e4409416d2f537d4c30fc11f012ea507f7bd74d3e19ecb57a", size = 852120, upload-time = "2026-06-28T19:54:25.972Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3e/3e31e255c4971f53cbce6306b5e3c76cbd3735a54f419bb3b2f194e9f68c/regex-2026.6.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:700fc6a7844bb2c4149292ac79d1df8841a00acd4d45cd32c1ebc7bcc1fd0da8", size = 789503, upload-time = "2026-06-28T19:54:27.678Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/d36561c21c3033d7eeb31d51b491916817de7861acefccc5fc9db8a5037c/regex-2026.6.28-cp312-cp312-win32.whl", hash = "sha256:03376d60b6a11aecb88a79fa2be06b40faa01c6693bc31ef69435cd4818b9463", size = 267109, upload-time = "2026-06-28T19:54:29.316Z" }, + { url = "https://files.pythonhosted.org/packages/a0/59/bbbb0591f38b18c65977cd65ce64749eba1c1996c99ac04e900fc30c0dcb/regex-2026.6.28-cp312-cp312-win_amd64.whl", hash = "sha256:fbd2ded482bf99e6651992bbfcde460272724d4bbc49ef3d6b46d9312867ec84", size = 277711, upload-time = "2026-06-28T19:54:31.143Z" }, + { url = "https://files.pythonhosted.org/packages/86/06/be4f6b337d773ae5739a1bc238f97c16926e72017243735853c030f4c628/regex-2026.6.28-cp312-cp312-win_arm64.whl", hash = "sha256:37294d3d7ddb64c7e89184b2894e0f8f0a19c514bc59513d71fe692c3a8d5fc6", size = 277022, upload-time = "2026-06-28T19:54:32.97Z" }, + { url = "https://files.pythonhosted.org/packages/b6/53/d5c1b3cc0b5a0c985563ad6fac93d73ff2b300cb84342d89f044625d6bc7/regex-2026.6.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b295a83426e0e44e9e60fde99789e181bd26788a1890ae7fe2a24c69bb6246ca", size = 490329, upload-time = "2026-06-28T19:54:35.775Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9f/0c3503e819e91ca0e7a901a8e989ebf840ac7c7aea20b1fc7f31b6759f77/regex-2026.6.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c31665c0deb5c111557a1cac8c27bd5629e2f9e7fd5058900a03576c33b601c", size = 292039, upload-time = "2026-06-28T19:54:37.977Z" }, + { url = "https://files.pythonhosted.org/packages/bb/7f/cd004e13fcad23b3794a82307dfd222e6365eb7f598bd3caab148a830bff/regex-2026.6.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6bf295f2c59de77d1ea7de053607ae4dc9ceb3d57bbb6c7ec51ef4acc4ccff94", size = 289488, upload-time = "2026-06-28T19:54:39.545Z" }, + { url = "https://files.pythonhosted.org/packages/73/4c/293fb34586fbcdc47eac436069e9c11f71fae5dadfd4889b475d7d2e5f7a/regex-2026.6.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17c077586770f67e05bbffeba07fbee6b2b22244f4d4caf8d94e59d574befe04", size = 796772, upload-time = "2026-06-28T19:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/c0cd1a90b7d12d9dc155cfc8bdea8df9720988ea5b07e8fa1eccbd0ab2dd/regex-2026.6.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e6cb5a61486f9062397d2e189573b39d38ecfaed698fd9fb6e2756a8ebb8762", size = 865467, upload-time = "2026-06-28T19:54:43.485Z" }, + { url = "https://files.pythonhosted.org/packages/4e/db/0b479973046d005a1eaea299d5d536aeecb9488a16d9cbb8286338102e2d/regex-2026.6.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e86e91a2664f44c3a4e363a7d78fb17c27d5046882e30ea5a877f5e89b28d2ba", size = 912345, upload-time = "2026-06-28T19:54:46.091Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5b/d65adfbd02f32212431bca1f06d1e2eb763a20b12978b454bafaf23dacb7/regex-2026.6.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dfd1331c49233998d84fc5f1f4436cf7a435a7655f6cf0f490229bb5c7254e5", size = 801291, upload-time = "2026-06-28T19:54:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/fc/09/2103686defaf9a0a31c1663782359d5b45f42524c64cca681f5481e44a5e/regex-2026.6.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cadea12805a1bce0b091c302b814207be26fb60a9c0e7f9ad2f9e21790a429fe", size = 777106, upload-time = "2026-06-28T19:54:50.326Z" }, + { url = "https://files.pythonhosted.org/packages/85/5a/b57593c0aa23ed269ec332fbcf07852abcb6b746e811d9464e0d09b4e25f/regex-2026.6.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f2c1682b67ad5d2376498f2a5a2a8f782fa2e4a06d0465b5e357799806e8a20", size = 785175, upload-time = "2026-06-28T19:54:52.172Z" }, + { url = "https://files.pythonhosted.org/packages/79/59/c36e756ad29bf14d7b6c6d7138952476b21f6160286cedb98ac13481c993/regex-2026.6.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:64e142eb55e84868087da1375d7c36ff97d55010951849f515322a91d5fef1b4", size = 860186, upload-time = "2026-06-28T19:54:54.11Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/49808aea0da9649c300139360708fb91b7144be1f962fcebf96755fde948/regex-2026.6.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:abb4daabe7be63273787a62dfd6164dadf8f7a63fbec3d2730e5e5e7126d858c", size = 765754, upload-time = "2026-06-28T19:54:56.04Z" }, + { url = "https://files.pythonhosted.org/packages/be/c5/52bbd436cf2200decdf48825fa38363eaaeebb77011ea9928a1ef9e0b9f2/regex-2026.6.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec2b2ad00ab8c16a2798cc8db80c53c4d5b8b3a2441f6cbaef06625f5ca25854", size = 852085, upload-time = "2026-06-28T19:54:57.988Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c3/0390b66e3019497143fe768b3ba567b64d8b24f3812d09506deb86f4a0f0/regex-2026.6.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bfc9677982c914d9085b8e1c3b3ae6e88f139fb56531c2416d6c8f338093c22b", size = 789600, upload-time = "2026-06-28T19:54:59.977Z" }, + { url = "https://files.pythonhosted.org/packages/88/fd/ab5b03653a244975069fed93d73f4f5f7484c03a84cedb238292510d7182/regex-2026.6.28-cp313-cp313-win32.whl", hash = "sha256:bf54bc693fc4e0530e666ba5ec4bcba14dbe8f66b7cfc15c27317d1a6e40b9a5", size = 267088, upload-time = "2026-06-28T19:55:02.159Z" }, + { url = "https://files.pythonhosted.org/packages/68/55/21022f7d3143210ae8d4ff905c45306237b657375cc0b97883f49db3d423/regex-2026.6.28-cp313-cp313-win_amd64.whl", hash = "sha256:e128feaf65bf3d9eb91bec92322a8f7e4835e9c798f3e9ea4b69f4def85620e3", size = 277680, upload-time = "2026-06-28T19:55:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/7f664804f1aef924542b0b233996b78b3e4d0a52d9951358aac99f129f51/regex-2026.6.28-cp313-cp313-win_arm64.whl", hash = "sha256:695873e0ea8d3815ea9e92e2c68faf039cc450e2c0a62a31afe2049eb11be767", size = 277017, upload-time = "2026-06-28T19:55:06.29Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e1/9eb83518e159d719fd681c4932dc2aaff855ce72451e1d05d69466f25a96/regex-2026.6.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:189dbf9fc4252d9f1352bf4bd1bef885edb6cc4b7341df202a65f821aaa3891c", size = 494195, upload-time = "2026-06-28T19:55:08.292Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e2/e259c5f2f7be269d0e2fb54275c1fa6a13fb47019f389c3f3ae457447825/regex-2026.6.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9277a4c6503390aa39cb4483b87ec0384faee0850a23b5cea33d008b5d8d83f1", size = 293976, upload-time = "2026-06-28T19:55:10.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/9bdf444014d22b045d0c82ca114fac7e07a597b5b5331b7c4ce6328426e2/regex-2026.6.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17eddca4e8ea9af0b5739314776cdf0172a49731ab61f2e1ea66e066ddd46c97", size = 292340, upload-time = "2026-06-28T19:55:11.88Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3a/f49b11e59cbfe187ace0053a460bd72a0169b8cd52e7db9421a074ce7a43/regex-2026.6.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4466b8641e00c697aab5a73150150d2b2ea96b131c595691f42031abafd9f4d", size = 811704, upload-time = "2026-06-28T19:55:13.612Z" }, + { url = "https://files.pythonhosted.org/packages/2f/fb/ad04c39e149bf8b6cf357df5fff78341733ec366780a00c803a36735818c/regex-2026.6.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cfcd4b0bdcf768c498415c170d1ed2a25a99bf0b65fa253bbd02f68ceba6475", size = 871157, upload-time = "2026-06-28T19:55:15.797Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/0e5ba31c11eb8ef7aac19a690c1211fc9aa9990caf09565785ebb0081b9a/regex-2026.6.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80c7adf1ef647f6b1e8aa2ca280e517174cd08bdf7a2e412cdfb68bd6a0917cb", size = 917287, upload-time = "2026-06-28T19:55:18.692Z" }, + { url = "https://files.pythonhosted.org/packages/11/75/6b78df2b858c2fcbbc4858fdc3f2975cf2703be374b2842db7d2c32591a7/regex-2026.6.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a043f5770e82283a22aed4cefef1a4e0f9dd8fd7184cb6ce0ad2e579e2134a9e", size = 816333, upload-time = "2026-06-28T19:55:20.973Z" }, + { url = "https://files.pythonhosted.org/packages/b4/01/ecfe665a3694d5eda9f3ec686c856438ada0943947b6005e90556a1e2cdf/regex-2026.6.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3bd630a8dba06b55254ea5ee862194edab52ec783100d2ef1cd15a9c512fee27", size = 785518, upload-time = "2026-06-28T19:55:23.003Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0a/88f9cd88ff1e82881605c4ffd62d77ee67d051232cfe6f8e9a64b86cf0e8/regex-2026.6.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b77207e3cee13086f1906a6a2a12b41244c577e8ad9370d4b35ae1d548d354f3", size = 801371, upload-time = "2026-06-28T19:55:24.888Z" }, + { url = "https://files.pythonhosted.org/packages/a8/97/601483732f93275482ceb9fed57813dfed7c47d3a019db6ec4a3bb6e23e0/regex-2026.6.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:6de82c268e5d101ee9e3ffd869924aa9a371e3a21e752cf4fa17b6ce50d219f7", size = 866517, upload-time = "2026-06-28T19:55:27.232Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/385c2a0351b994a693453c1d1a6e9af9eb35db3c9460d76b5078acd70c62/regex-2026.6.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b15859e3908544fb99cf47341dcf0bfd089147d258c4c4d8a29e5b087f8085cb", size = 772834, upload-time = "2026-06-28T19:55:29.154Z" }, + { url = "https://files.pythonhosted.org/packages/06/bc/bbf4a5b3b29770d7f307d3c28b5b1bca0105b0cb424be0a4eb1339bc92cf/regex-2026.6.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c91487a917edd48a1ea646fdf60d7936d304f0e686fa7ea8326e47efca51d816", size = 856606, upload-time = "2026-06-28T19:55:32.186Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/51d74fff82f682819979249f8d700267108ba5dc4eb284b0e11b9c85e4b3/regex-2026.6.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4ac65f3e3a99fd8f3a4a74e7a6610acd1ce9dfe9b8a03d346a4922380d68aeb", size = 803475, upload-time = "2026-06-28T19:55:34.328Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3e/6be10cefdc813533fe604dbf5d3c77d2638e7ee658b2749ebadc113b6b2e/regex-2026.6.28-cp313-cp313t-win32.whl", hash = "sha256:3f6316f258bc7e6c9c2acbe9954947bbd397a81be3742a637a555f1855d6618d", size = 269126, upload-time = "2026-06-28T19:55:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3c/32cda905ea1a6eeeb798291c294d8ec66ee0efe0cdba28b061e248b1d396/regex-2026.6.28-cp313-cp313t-win_amd64.whl", hash = "sha256:1484bdd6fba28422df9b5ebb04055b2e1b680e8e4f08490bb21ff0f3cc50d0ab", size = 279961, upload-time = "2026-06-28T19:55:38.456Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b9/69f4e5cd6fbe0bb420cb2dbae441ca118f2495bdda522a74da75aa9829e7/regex-2026.6.28-cp313-cp313t-win_arm64.whl", hash = "sha256:3f15020f0b69cafe57baa067ff65b29acef68ff6b1670a53bef1ca11d708e02d", size = 279266, upload-time = "2026-06-28T19:55:40.62Z" }, + { url = "https://files.pythonhosted.org/packages/3b/fb/fad3b810a5bb1e09b9e5d6913fc6ba88cab738fdf283196827a3c59a4c10/regex-2026.6.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:f7c032b0c8a73739ff8ff1aaf30c281fa19c17bf7f1543256c8507390db7807c", size = 490407, upload-time = "2026-06-28T19:55:42.724Z" }, + { url = "https://files.pythonhosted.org/packages/d6/52/b8c79d12276d93e90e707e939b396034c04980caf1235312ef790f8e11fc/regex-2026.6.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f6710f512c57b84f127a23d0f59560a03b64136eff419ae1be5ab557577fe5e3", size = 291988, upload-time = "2026-06-28T19:55:44.549Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/6a911f18279daa8d7bb8b20d771ddb6ef31fabd35f5921f9d3ba21640e80/regex-2026.6.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0013958f427bd82509a186b9ff206d66cb8d60a81fc797a4c717afd18c5b0ba", size = 289704, upload-time = "2026-06-28T19:55:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/fd/22/ad1955c47c669291a05804d53d7071cc0732dfdf166857be38003cedc2d1/regex-2026.6.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f06cdcd6421f8e194ad312ea608020381250df9b8a57661c1b57e9e5273878", size = 797017, upload-time = "2026-06-28T19:55:48.166Z" }, + { url = "https://files.pythonhosted.org/packages/e5/67/a83159ff8703ab4d0c2cf99e76ebf289b7b4a501623241d09f88f3614f80/regex-2026.6.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec9689392f7494ff4e3f8e7e8522f9158f11023f337eaaf04a64542fc45bbf26", size = 866112, upload-time = "2026-06-28T19:55:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/7bff2d6dbbd77421b3274aa51db1c887381cbc5b6eda93598c3e882ea345/regex-2026.6.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aa084684e6d2078bf6139e374d1fc2af5ddc1ac7122759a2db716d68169f6fd0", size = 911554, upload-time = "2026-06-28T19:55:53.707Z" }, + { url = "https://files.pythonhosted.org/packages/29/44/ae59c3826e7ba492e56795cdf74ea2a7b5b7c5ea116afb79ee4956a5dff1/regex-2026.6.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40455e6840dc4e96a6fe50f4cedc957de2752c954d91e789812be55d49be199a", size = 800665, upload-time = "2026-06-28T19:55:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/d6/19/6fd033d2ab00f35d445aaeaf3307c1e721424dcbfd48f6f65c857cb939cf/regex-2026.6.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:530b5c223b9ca5dd8370ac502e080aee0e4ded32be987c6564b425fb5523d581", size = 777243, upload-time = "2026-06-28T19:55:57.909Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9d/99730f26df4938049ab1e652ca75e967b4c6739444e18d9707bfdb8af20c/regex-2026.6.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ed273ecd1a89be84466c1749bfe58609cc2a32b5d5e05006c4625ba96411b", size = 785784, upload-time = "2026-06-28T19:56:00.072Z" }, + { url = "https://files.pythonhosted.org/packages/48/49/105cd57162f5fc5c04cc917a1388a060cf8427e5c14353cd9044660fbf4d/regex-2026.6.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ab0d5344311fc8e8667078942056c3b9c9b4a4b1cc99f2eb8a5af54554f4acc", size = 860914, upload-time = "2026-06-28T19:56:02.017Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/788245a95b69018f58bff2f4fd27d007cacaea088cdb390979743f1b2571/regex-2026.6.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:eacb79625323d9f7e7925366b917f492b8356fad58f5dc4fa12ff8c21d8f4ca9", size = 765915, upload-time = "2026-06-28T19:56:05.021Z" }, + { url = "https://files.pythonhosted.org/packages/ca/01/292065a39a004b05e67a337b18213670a7cb919d6856ac2d7df7f1a10dbb/regex-2026.6.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20f4d87702702aa1d572721e146f301660c50eef6fd6cb596e48a22b0ace17db", size = 851404, upload-time = "2026-06-28T19:56:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/98/9e/a93d865db0e13483ae1a01d81e2ce16d4a7fe2f9b9fe4aac4cc08590b136/regex-2026.6.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e693940a3b9e6d6e4dc2a54ecaa74b74934f77af1ef95f518a74261ef7cc1bc", size = 789373, upload-time = "2026-06-28T19:56:09.894Z" }, + { url = "https://files.pythonhosted.org/packages/82/0c/38b1685ad4017d78efbc8fa7dbbf96d8113b53750c8aa2d3609defd46605/regex-2026.6.28-cp314-cp314-win32.whl", hash = "sha256:234a51e20ebc18ab83b2c0600cf28f2e884560a0e00f743878f0b7d8e7c4cf03", size = 272496, upload-time = "2026-06-28T19:56:11.83Z" }, + { url = "https://files.pythonhosted.org/packages/55/50/e19f261ff9ba9b50722a529e09b1743ecf65eb348be99d0fd2cd7fcede1c/regex-2026.6.28-cp314-cp314-win_amd64.whl", hash = "sha256:7b15c437bc4604f03ceb3f8d37eae2f8930e320e1bc556b259848c639d9eec1a", size = 280754, upload-time = "2026-06-28T19:56:13.758Z" }, + { url = "https://files.pythonhosted.org/packages/36/b8/c9e68f3a9e33be73f20990b2c065b144ff2d0aa242608a950d8c4f3b56e8/regex-2026.6.28-cp314-cp314-win_arm64.whl", hash = "sha256:c6e6f790d01380a74ad564f216c533b86504afb61bf66f2b2e11e7f1a3e287a7", size = 280979, upload-time = "2026-06-28T19:56:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/03/e6/21c425a37880c650d007c4171c6a80325446d830d85f5fbf335e7205b1e7/regex-2026.6.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3527a72adcbe9e3600f1553b497d397c1a371d227580d41d96c3c5964109b65c", size = 494282, upload-time = "2026-06-28T19:56:18.049Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/6647a7ccf5ffff995ba955a0b7d766440f4e58ce1666549c8ee998f2b972/regex-2026.6.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a644f6408692812f5ead82519eed680e08d5d546fddbd9f7d9514e3c73899aa5", size = 293977, upload-time = "2026-06-28T19:56:20.145Z" }, + { url = "https://files.pythonhosted.org/packages/8c/dc/a3e141a4eaf125e50f63105570c01fa477c06ac5259dcfa95e9b90760e84/regex-2026.6.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8e2fae6bb883648346f84db270dc9aafc29d8e895f62b88a75ccc83b09519820", size = 292432, upload-time = "2026-06-28T19:56:22.345Z" }, + { url = "https://files.pythonhosted.org/packages/35/ee/2ac1a6b9f167f8ff69f5a789938cc103b60cff41b24a6990daced8b88e34/regex-2026.6.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debe623e09cee97ef9404575e936c610aac9bb08358c5099aaef14644a6871f2", size = 811877, upload-time = "2026-06-28T19:56:25.056Z" }, + { url = "https://files.pythonhosted.org/packages/df/7b/9a5505ee92180bcae300b1018b9ff3d3c19962436e66f2505f255e9fde35/regex-2026.6.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc579c91fb4605773483a8d940b136bcc5b854fff44fa14a1572a038f46563f1", size = 871212, upload-time = "2026-06-28T19:56:27.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/4d/d61a702a9f9d1bd29b22cbef1aed6d477baa961232a7eb4d91b7775b0b3e/regex-2026.6.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7c42be203d84ecf7d487ff23f8a61ef0eb0534fa0fc317a2fce8c065d20618f", size = 917507, upload-time = "2026-06-28T19:56:29.762Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/1308066f5966b65fbb6905b99ba37e9f1cd753dd0ac08485f8257334ee92/regex-2026.6.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8184b4e2fdaf9cdfe77e38f15a4d9dc149168c9c29eb0ea17c5481d3bb80546", size = 816389, upload-time = "2026-06-28T19:56:32.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/57ce2cb8d714ee0b7f11c7ee4cfe2af66df2b90f147feadcb538609a3a02/regex-2026.6.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:697f103104f5872d64078d8eeac59979960be8ee76115a2d3f31096312e2a400", size = 785890, upload-time = "2026-06-28T19:56:34.492Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fd/1d5350d3a8a327bff0fccacb911732baf7b5b6f5529c0e3fa602a23e7dad/regex-2026.6.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:714d2b1aa29beef0ddfcdc72ad0771c05326551a8bb0680b0ddf74bfaad87387", size = 801451, upload-time = "2026-06-28T19:56:36.749Z" }, + { url = "https://files.pythonhosted.org/packages/f3/79/3c9e4f8a0306e030ad5a43bbbc01625fb28d58a813bc52d42fd1cc63fb2e/regex-2026.6.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0f09f62e450cc2f113018cc8412aeea3a120a04e1ca7e801a0d441583f9a3b06", size = 866504, upload-time = "2026-06-28T19:56:38.994Z" }, + { url = "https://files.pythonhosted.org/packages/65/12/f747de475b54f4709efb24dd0fbc8467c64cec91f5db0d047b079646ee78/regex-2026.6.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:731ea12d5aeb2577eaef2393d6428b995f76eb35f68a89e03e15a97719d1de19", size = 773047, upload-time = "2026-06-28T19:56:41.061Z" }, + { url = "https://files.pythonhosted.org/packages/58/3c/f02f860e0500c1b2d61a79dec7e214b37fb9656281dcddc92397edf96678/regex-2026.6.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:51e952c8783eabd4706d0f63922f219bcfc1bef9b8cb35941c0d1a0396578858", size = 856665, upload-time = "2026-06-28T19:56:43.466Z" }, + { url = "https://files.pythonhosted.org/packages/4d/6c/28b3fa222513484be9dee26b7222bda109056c43ea28aa2314262ca48816/regex-2026.6.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43248fe4c0ab8fbb223588a0795b11268940072c97bba30ea8f9b49d8cdfde34", size = 803573, upload-time = "2026-06-28T19:56:45.791Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/8f86cf1a1fd85c5ab0c503c9fe4607ad4ad48978b2d8b435d94465e134c7/regex-2026.6.28-cp314-cp314t-win32.whl", hash = "sha256:fc1eddc25ad23c0f1344ab280d961ac595ead48292d7c779497975942373f493", size = 274515, upload-time = "2026-06-28T19:56:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/0f/de/f8613c03b36786ddef2c930d28f9bcae861fcd541cc9203a870956cf1e83/regex-2026.6.28-cp314-cp314t-win_amd64.whl", hash = "sha256:ede8d8e53b6dde0a50f7eca902f0af76d87ab02a55aba7542da68ae3e5dfe83d", size = 283650, upload-time = "2026-06-28T19:56:50.614Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f3/f5ec86839bbabe33b6dee649b62ff9a445d43de6b0ad780cf6b83c56f61e/regex-2026.6.28-cp314-cp314t-win_arm64.whl", hash = "sha256:4da6f6a72f8700b97a1a765e837fb7d5750bfd9f13acea7bae498f573e3a70a8", size = 283338, upload-time = "2026-06-28T19:56:52.879Z" }, ] [[package]] @@ -2532,27 +2538,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, - { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, - { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, - { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, - { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, - { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, - { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, - { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, - { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, - { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] [[package]] @@ -2671,45 +2677,45 @@ wheels = [ [[package]] name = "treestamps" -version = "4.0.2" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/a1/02de5cb2900dbaefed89971b3c5bc61a9ee93c13d68725b61022e0d575c8/treestamps-4.0.2.tar.gz", hash = "sha256:7ab0ae03b25ddcf45011cc0e589b9137baeb2e284f0b194f5cf7a299a8baa9bb", size = 181276, upload-time = "2026-04-29T09:52:32.935Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/87/31328b19121029e6db528aa4c70f992f1dd5ccc74435bb2e22874970f64d/treestamps-4.1.0.tar.gz", hash = "sha256:159652cd3a2f6365e3d7c5021defb66104ba89148c0cdd87e7ec14f713870cb3", size = 210180, upload-time = "2026-07-05T19:46:29.661Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/10/d467b3fc53990fb3f86a5cffd0a290d717fa076442557b585af882a2c716/treestamps-4.0.2-py3-none-any.whl", hash = "sha256:53146741ace2a8df626d2f305cfa3aac8c7719e4af26a6dcb9aa19ba270aecb1", size = 18358, upload-time = "2026-04-29T09:52:34.07Z" }, + { url = "https://files.pythonhosted.org/packages/3d/cb/508cbbb17a23ad35cea3fe2b86c8b1811d0c37fd7476c1e3892e79025be5/treestamps-4.1.0-py3-none-any.whl", hash = "sha256:f7323de89fad48030989cee9939b809c5ede15c5a20d5153c157f8d008231494", size = 32155, upload-time = "2026-07-05T19:46:28.62Z" }, ] [[package]] name = "ty" -version = "0.0.40" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/f8/a754c96967b71de8723f88be17df8738216bd382ffed229cd500b7a24d13/ty-0.0.40.tar.gz", hash = "sha256:883b53dd98f6e5b33ab1c8e1a3cd94b0f29c762ef22cdf1e86aaffb4fd711c67", size = 5726484, upload-time = "2026-05-27T17:55:43.615Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/42/d029a72165ad39f95228b67355927fbd35c821dc8e3e475d49f47c2eeb1e/ty-0.0.40-py3-none-linux_armv6l.whl", hash = "sha256:9defb4742450e569a6a09de286a04008d6c2e815112da4362c88b6eaa2f52a36", size = 11406372, upload-time = "2026-05-27T17:55:49.633Z" }, - { url = "https://files.pythonhosted.org/packages/23/99/7f8ea09b7e49afbf795cb3341a3217f30f228db7e62a2268ed8cbbf813d6/ty-0.0.40-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:868258a3330db88b683fcafe2c4e936d6226a6312799bf15b585d93557b2d38c", size = 11159782, upload-time = "2026-05-27T17:55:47.405Z" }, - { url = "https://files.pythonhosted.org/packages/04/d8/1ea745ee97a98b26ae9564d19a430a76a35297cd450e84dcaad22e1f7ee8/ty-0.0.40-py3-none-macosx_11_0_arm64.whl", hash = "sha256:589c81060cf1e7a9ffa2f45bfa35ffd9b9fbd214104e3f13959f113627efcd91", size = 10594139, upload-time = "2026-05-27T17:55:37.206Z" }, - { url = "https://files.pythonhosted.org/packages/39/1a/fbef21273c6617ff4715b4827ee1c0b6550aa7d1df4b8c43b325545c1cf4/ty-0.0.40-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b06108990cb338d941c315ae6e9ba2fff8f518bc15d3f33e5619ff6a6c9beab", size = 11114156, upload-time = "2026-05-27T17:55:56.11Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f9/389fc4976d7ec016a7473cf1274bf9c4f491bb54c66649bd022bff9f2b6a/ty-0.0.40-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3913ef37336bec4f96bd2512f8c3a543ca34c259b7170f7eb5adf75b3ed7f04c", size = 11189050, upload-time = "2026-05-27T17:55:54.099Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a9/4ecabbf4bdda7df0d99d8d3892c6edac0efc8c4cae756a5109178a3d0e86/ty-0.0.40-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fd1486bd5fe48779a8aa857137f3642a0a9161f5cf57d4380f4a0ecea01c8f3", size = 11664266, upload-time = "2026-05-27T17:55:28.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/02/0aa78730116507c265afb1d6d5961c583b49d4c2e368c4a49fd81bcae6dc/ty-0.0.40-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1668364d5254a734329917ee66c2c5fdd5665389d41043f6fce0f22ddb32b749", size = 12187743, upload-time = "2026-05-27T17:56:04.337Z" }, - { url = "https://files.pythonhosted.org/packages/e6/68/ccabf2d173523598271a385c1d3f864dbda23e5ebdc67f5969b9e830ea05/ty-0.0.40-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f77a73edb91e5dfa2ab9af7c4cac64614f8cc121f38a8875f22e830d3aba6a", size = 11862999, upload-time = "2026-05-27T17:55:58.087Z" }, - { url = "https://files.pythonhosted.org/packages/03/8d/6d7ec22771bb23d534797cdb446eb644bccfe7a62b729bb99e7235a02fc3/ty-0.0.40-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1274ce0212ecbfed01bda7c3659c46e8bd0068e32d00c46c790466a95274c3df", size = 11743896, upload-time = "2026-05-27T17:56:00.017Z" }, - { url = "https://files.pythonhosted.org/packages/cd/a4/f9fa076b010c91cb249b1fcc3476569b7b8462cb4b688da2d04c23a0622f/ty-0.0.40-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5ee1261dbc363e5cc1a0c5bb0c8612c192bfe53491214df8bc85a540835685f9", size = 11883581, upload-time = "2026-05-27T17:56:02.319Z" }, - { url = "https://files.pythonhosted.org/packages/fd/0f/5b776a2328c756d574dd4d6afbd30fc24e1ab4b76935c7c3c23f27ebbcb9/ty-0.0.40-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6220e2cd5cdc4683dd87fb150d195bbd9f1a021395e04cb08bd3c66ea6da6ef8", size = 11093946, upload-time = "2026-05-27T17:55:33.284Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/eb23154bae83ad7c2935e9e5916660fb3e31598a92ee232aebd79410480c/ty-0.0.40-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:46b9ed69d01d98ef046afac9983c68336f572605ea2a27b90fbe6f80bfc8d6b7", size = 11210737, upload-time = "2026-05-27T17:55:45.523Z" }, - { url = "https://files.pythonhosted.org/packages/ff/19/1fb2529703f708cacfd13a89f98613cae2907dfa941b26976467e6119803/ty-0.0.40-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ddbca9fab4406260f141674ab5efcfe7b02bd468e6985e4cdde0a21626e69ffe", size = 11332563, upload-time = "2026-05-27T17:55:41.674Z" }, - { url = "https://files.pythonhosted.org/packages/87/69/b3f5a8ef26c31204e0391147b3adcdb0674eda3e7d99868478ef168a41c6/ty-0.0.40-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1fcc082a749e6dc11b68fe9aab0420238bbf2a2374c2c7aa3c22e8c1618b136", size = 11843216, upload-time = "2026-05-27T17:55:35.367Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e8/20193069d32787f3e1a6ec8940aaa3759d3de8f48f9281bcc0c5cb0939da/ty-0.0.40-py3-none-win32.whl", hash = "sha256:75feb115b3587824c5bdf8f8305e9547b0d1e398e3077b0addc7a1988ea9bb50", size = 10670731, upload-time = "2026-05-27T17:55:31.316Z" }, - { url = "https://files.pythonhosted.org/packages/a3/f9/8b2aa4da61db81322d4a2f9db227afeb48110ca15ae31d380f64c64ceb63/ty-0.0.40-py3-none-win_amd64.whl", hash = "sha256:b0f905edaad788bd61f779a85801b60a267a25ed57fca05aaddd168d9d8896be", size = 11766211, upload-time = "2026-05-27T17:55:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/04/87/369056ed46f1b235130ec0595393262f9cd2061ca3dab276d490980f9343/ty-0.0.40-py3-none-win_arm64.whl", hash = "sha256:07da2b09d9130e2c9a257d2a29beb53105835b0256ee5fdb288fe1aab83fee47", size = 11117369, upload-time = "2026-05-27T17:55:39.329Z" }, +version = "0.0.56" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/07/fb29aea5235b0aa8ecfc4d1cc6ddf9fba8b863d67d96c6d345694d644c43/ty-0.0.56.tar.gz", hash = "sha256:84d114dc3796361c0fc72945016eabd74d46b9ee64f198cb0e485719704681e5", size = 6050123, upload-time = "2026-07-01T16:44:56.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/48/bce79e7ca5c1cc529d3e0d37ddd1121aea4b68a4f749974ad1cc77161871/ty-0.0.56-py3-none-linux_armv6l.whl", hash = "sha256:186d4a53e15747c947e1ec3d7eec8e345d8e40a1ca10e634c585db52497e87dd", size = 11643066, upload-time = "2026-07-01T16:44:18.374Z" }, + { url = "https://files.pythonhosted.org/packages/80/d1/22555d8a1d719661f10050f3865d877bbf497da908961c75fe22217dd18a/ty-0.0.56-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aae1a980fd9535da0469b7ba2b2e1b54a907743a5e0f442dd57eee9f5bfd034c", size = 11407487, upload-time = "2026-07-01T16:44:20.956Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/b3b7a74ce8bc59ef48843ad80179bb0d9598bbd6cfc0d11d519bdf6b1352/ty-0.0.56-py3-none-macosx_11_0_arm64.whl", hash = "sha256:afd3058c0a6c5f241e814734f133008c93ee805f61c9cf4ce7412b8822b5d9ad", size = 10962270, upload-time = "2026-07-01T16:44:22.959Z" }, + { url = "https://files.pythonhosted.org/packages/64/ac/6c2fd7de0304a8a7218a756af74f7e62a5e8540fdb175e0a869e51042345/ty-0.0.56-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:058b52f7a823ac13aae3cae30809dd6b5145794b64d8478f9ef38c75d79b4483", size = 11471406, upload-time = "2026-07-01T16:44:25.327Z" }, + { url = "https://files.pythonhosted.org/packages/50/b6/11d861156861c03c7726b74558f9a0e0092661aff83a4fda1279df28c425/ty-0.0.56-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c66e00c1522add1f2bbdd2e45828c953b35c306b7bef03ec9169c75a63699a0", size = 11445612, upload-time = "2026-07-01T16:44:27.531Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ba/09df108582090f3c0770ec4bc8675affed60248f6793a78d909be16211d9/ty-0.0.56-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40903d71c669a30691b5a5d5728056c7877a1bd6be4f233a38883a8b28cf34d7", size = 12093889, upload-time = "2026-07-01T16:44:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f7/dbb4b4ccb69cd64c209ae55b1ab788ace8222c2bc1f6845be9e7cbedbf25/ty-0.0.56-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63fe3947fe0c46c69a7d950e6832ee70a9ec17321fefbff3d2e3c20baf9e5bd0", size = 12666337, upload-time = "2026-07-01T16:44:31.586Z" }, + { url = "https://files.pythonhosted.org/packages/86/e9/73f903fe4a3d9ea02f26f57c1eb07e3b1029ec92b0e8c2364718893440e3/ty-0.0.56-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71a0c1a72f9854532e710e119b6871ffe4542c8a65146f1f65dcd78fecd885b4", size = 12280247, upload-time = "2026-07-01T16:44:33.637Z" }, + { url = "https://files.pythonhosted.org/packages/d6/90/cebd222495832f1a00dcd321ba25f3cab804221a4991b992c2178bec68ee/ty-0.0.56-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70d1665596494e24d8ebd198438872b5a56ec3cae5f2bcf6c673be797acc4e3c", size = 11991107, upload-time = "2026-07-01T16:44:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/8f7337a07250f42d975cdb6decf47fc5b421e6c7da5e3e7be1e85f63a7e5/ty-0.0.56-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:778f99e51558afc1dbbe48ee38ab6aae7b31390ed8c1a1ef1499b295e9f1e82f", size = 12298970, upload-time = "2026-07-01T16:44:38.243Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b9/a52cd59034a48f5f18c6b155cc2cc36861d874b6d0af204b12c898024c3d/ty-0.0.56-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:867bc5708e0066bb4ff6c7db524bd5deea2676c62bfe71d3303138b3be850af0", size = 11425683, upload-time = "2026-07-01T16:44:40.473Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2e/48e42d33357d52eefb695c0c3fcfc96879b73668a7447d1d1e0ad774fedc/ty-0.0.56-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6012f4189c928edb330a37deb9930f982380bd4aa7c4b8e0428eec9651c7551", size = 11469258, upload-time = "2026-07-01T16:44:42.513Z" }, + { url = "https://files.pythonhosted.org/packages/d5/01/ad1b4138be1e3fa97863af3925aa2134f17a593240c35dc38c3429fb5ad1/ty-0.0.56-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee83de1a7ff4cc32837ec06134ce391d441bc5b35ecd8d3cfe053f120f3e4c1", size = 11758736, upload-time = "2026-07-01T16:44:44.567Z" }, + { url = "https://files.pythonhosted.org/packages/09/34/9d81967ff240eaa57e9249728ef7b7790747cf6d3c9a98ec86b2cfdcc8ee/ty-0.0.56-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:62619b3b0e2c6248ef30d3f0e2f2217ae9893040585be07f32324242f197cd6f", size = 12100242, upload-time = "2026-07-01T16:44:46.584Z" }, + { url = "https://files.pythonhosted.org/packages/c3/36/f51d4666d2de6cf33c1f3a1fcc4bb6b70b197dd6ceaa491eef71d78fe8e8/ty-0.0.56-py3-none-win32.whl", hash = "sha256:b30687bb5cd9729d34c889a289edf32770388d9bb05243e534e723fb45e0381b", size = 11093759, upload-time = "2026-07-01T16:44:49.171Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b4/8fb5d4acfa4afb152245b20fa263069a7547bd1f8e4bfca4eda280c897d7/ty-0.0.56-py3-none-win_amd64.whl", hash = "sha256:ad4c8c47b6f4e3f9ed3fc0b1a5d650088d229e17dd8f63c1826d6bbe94cc3235", size = 12100327, upload-time = "2026-07-01T16:44:51.26Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" }, ] [[package]] name = "typer" -version = "0.26.2" +version = "0.26.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -2717,9 +2723,9 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/a5/756f2e6bc81a7dd79aa3c625dd01b74cabc4516628cace2caaec09ca6ff2/typer-0.26.2.tar.gz", hash = "sha256:9b4f19e08fcc9427a822d1ef467b1fe76737a2f65c7926bdeba2337d73569b68", size = 198991, upload-time = "2026-05-27T10:41:39.166Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/a5/6ffd702beda8798b2b82ff70805ed4a66d963557e43a5d1823ab456251a4/typer-0.26.2-py3-none-any.whl", hash = "sha256:39beff72ffbb31978a5b545f677d57edb97c6f980f433b38556deb0af25f094d", size = 123123, upload-time = "2026-05-27T10:41:40.504Z" }, + { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, ] [[package]] @@ -2733,24 +2739,24 @@ wheels = [ [[package]] name = "typeshed-client" -version = "2.11.0" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-resources" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/7d/62fbae352d5fb7ce5ef4d9ca73bf7a9b02b790d2524ab6ef1e0e799a5d1b/typeshed_client-2.11.0.tar.gz", hash = "sha256:0b8f2ab88f611f5e97b70d2a8123942d3d7d5c74cee8ae694db83422f32f9481", size = 522774, upload-time = "2026-05-01T14:51:52.38Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/5d/97d6fa8c204b0d42be931e7a568b306cda43f93ff45f8b47537ee61390de/typeshed_client-2.12.0.tar.gz", hash = "sha256:54dcfa25fcff82cedcb1b51c03c1b3c51a614097aab8fe49e4316aff6f79d9c7", size = 529457, upload-time = "2026-06-02T04:00:51.51Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/22/fa16b462157bd869dfad528f5637506b9430ca63d48fb536ecf4cc78481a/typeshed_client-2.11.0-py3-none-any.whl", hash = "sha256:5745e0990b80b29a286b22d68f81779c5c7adf1cac8969eeafba44b73b486c36", size = 787609, upload-time = "2026-05-01T14:51:51.005Z" }, + { url = "https://files.pythonhosted.org/packages/85/98/3aef1587ec9b3a5cd062b8d8910545a0ee3715c31fd65b1f2386b27b94af/typeshed_client-2.12.0-py3-none-any.whl", hash = "sha256:da3ef54a0e60c0f45f59006b73a3cb20b1acbf7a784e982476d8193e8828ddf5", size = 796929, upload-time = "2026-06-02T04:00:49.643Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +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/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { 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]]