diff --git a/NEWS.md b/NEWS.md index bb84f654..e9998336 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # 📰 Picopt News +## v6.3.0 + +- New progress and logging + ## v6.2.0 - Add bunx support for svgo diff --git a/bin/roman.py b/bin/roman.py index c65041cf..54063e5b 100755 --- a/bin/roman.py +++ b/bin/roman.py @@ -77,6 +77,38 @@ def has_description_comment(line2: str) -> bool: return bool(COMMENT_PATTERN.match(line2)) +def _scrutinize_sub_path(sub_path: Path, root: Path, spec: PathSpec) -> Path | None: + if not sub_path.is_file(): + return None + try: + rel = sub_path.relative_to(root) + except ValueError: + rel = sub_path + + # Match against each component so directory patterns work + if spec.match_file(str(rel)): + return None + + return sub_path + + +def _scrutinize_file(path_str: str, spec: PathSpec) -> Path | None: + path = Path(path_str) + if not path.exists(): + print(f"👎 Path does not exist: {path}", file=sys.stderr) # noqa: T201 + sys.exit(2) + + root = Path(path).resolve() + if root.is_file(): + rel = Path(root.name) + return None if spec.match_file(str(rel)) else root + + for sub_path in sorted(root.rglob("*")): + if return_result := _scrutinize_sub_path(sub_path, root, spec): + return return_result + return None + + def iter_files(path_strs: Sequence[str], spec: PathSpec) -> Generator[Path]: """ Yield every file under *roots* that is not excluded by *spec*. @@ -85,31 +117,8 @@ def iter_files(path_strs: Sequence[str], spec: PathSpec) -> Generator[Path]: that gitignore-style directory patterns (e.g. ``vendor/``) work correctly. """ 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 - sys.exit(2) - - root = Path(path).resolve() - if root.is_file(): - rel = Path(root.name) - if not spec.match_file(str(rel)): - yield root - continue - - for sub_path in sorted(root.rglob("*")): - if not sub_path.is_file(): - continue - try: - rel = sub_path.relative_to(root) - except ValueError: - rel = sub_path - - # Match against each component so directory patterns work - if spec.match_file(str(rel)): - continue - - yield sub_path + if return_path := _scrutinize_file(path_str, spec): + yield return_path # --------------------------------------------------------------------------- diff --git a/bin/sort-ignore.sh b/bin/sort-ignore.sh index dde27767..971a30f5 100755 --- a/bin/sort-ignore.sh +++ b/bin/sort-ignore.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash # Sort all ignore files in place and remove duplicates +# Set locale to make output deterministic across shells +export LC_ALL=en_US.UTF-8 for f in .*ignore; do if [ ! -L "$f" ]; then sort --mmap --unique --output="$f" "$f" diff --git a/bun.lock b/bun.lock index 106f258e..d9f77739 100644 --- a/bun.lock +++ b/bun.lock @@ -22,10 +22,10 @@ "eslint-plugin-mdx": "^3.7.0", "eslint-plugin-no-secrets": "^2.3.3", "eslint-plugin-no-unsanitized": "^4.1.5", - "eslint-plugin-no-use-extend-native": "^0.7.2", + "eslint-plugin-no-use-extend-native": "^0.7.3", "eslint-plugin-perfectionist": "^5.9.0", "eslint-plugin-prettier": "^5.5.5", - "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-promise": "^7.3.0", "eslint-plugin-regexp": "^3.1.0", "eslint-plugin-security": "^4.0.0", "eslint-plugin-sonarjs": "^4.0.3", @@ -378,13 +378,13 @@ "eslint-plugin-no-unsanitized": ["eslint-plugin-no-unsanitized@4.1.5", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg=="], - "eslint-plugin-no-use-extend-native": ["eslint-plugin-no-use-extend-native@0.7.2", "", { "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" } }, "sha512-hUBlwaTXIO1GzTwPT6pAjvYwmSHe4XduDhAiQvur4RUujmBUFjd8Nb2+e7WQdsQ+nGHWGRlogcUWXJRGqizTWw=="], + "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-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-prettier": ["eslint-plugin-prettier@5.5.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.12" }, "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"] }, "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw=="], - "eslint-plugin-promise": ["eslint-plugin-promise@7.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA=="], + "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=="], diff --git a/cfg/gha_std.mk b/cfg/gha_std.mk index cd930b33..d66c0ea6 100644 --- a/cfg/gha_std.mk +++ b/cfg/gha_std.mk @@ -2,5 +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/package.json b/package.json index ec2930fa..d10ca2ce 100644 --- a/package.json +++ b/package.json @@ -86,10 +86,10 @@ "eslint-plugin-mdx": "^3.7.0", "eslint-plugin-no-secrets": "^2.3.3", "eslint-plugin-no-unsanitized": "^4.1.5", - "eslint-plugin-no-use-extend-native": "^0.7.2", + "eslint-plugin-no-use-extend-native": "^0.7.3", "eslint-plugin-perfectionist": "^5.9.0", "eslint-plugin-prettier": "^5.5.5", - "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-promise": "^7.3.0", "eslint-plugin-regexp": "^3.1.0", "eslint-plugin-security": "^4.0.0", "eslint-plugin-sonarjs": "^4.0.3", diff --git a/picopt/cli.py b/picopt/cli.py index fc1ec53e..0144e3aa 100644 --- a/picopt/cli.py +++ b/picopt/cli.py @@ -10,7 +10,8 @@ from typing import TYPE_CHECKING, Any from confuse.exceptions import ConfigError -from termcolor import colored +from loguru import logger +from rich.console import Console from typing_extensions import override from picopt import PROGRAM_NAME @@ -18,7 +19,8 @@ from picopt.config import PicoptConfig from picopt.doctor import PicoptDoctor from picopt.exceptions import PicoptError -from picopt.printer import Printer +from picopt.log import setup as setup_logging +from picopt.log.styles import MARKS from picopt.walk.walk import Walk if TYPE_CHECKING: @@ -120,37 +122,40 @@ def _comma_join( return result -COLOR_KEY = ( - ("skipped", "dark_grey", []), - ("skipped by timestamp", "light_green", ["dark", "bold"]), - ("copied archive contents unchanged", "green", []), - ("optimized bigger than original", "light_blue", ["bold"]), - ("noop on dry run", "dark_grey", ["bold"]), - ("optimized in same format", "white", []), - ("converted to another format", "light_cyan", []), - ("packed into archive", "light_grey", []), - ("consumed timestamp from archive", "magenta", []), - ("WARNING", "light_yellow", []), - ("ERROR", "light_red", []), +# Order + label for each mark in the help epilogue legend. The char and +# style are pulled from the centralized MARKS table so the legend can +# never drift from what the bar actually renders. +CHAR_KEY_LABELS: tuple[tuple[str, str], ...] = ( + ("skipped", "skipped"), + ("skipped_timestamp", "skipped by timestamp"), + ("copied", "copied archive contents unchanged"), + ("lost", "optimized bigger than original"), + ("dry_run", "noop on dry run"), + ("saved", "optimized in same format"), + ("converted", "converted to another format"), + ("packed", "packed into archive"), + ("consumed_timestamp", "consumed timestamp from archive"), + ("warning", "WARNING"), + ("error", "ERROR"), ) def get_dot_color_key() -> str: - """Create dot color key.""" - epilogue = "Progress dot colors:\n" - for text, color, attrs in COLOR_KEY: - epilogue += "\t" + colored(text, color, attrs=attrs) + "\n" - - epilogue += ( - "\n" - + colored("doctor mode:", "blue", attrs=["bold"]) - + "\n " - + colored(PROGRAM_NAME, "light_magenta") - + " " - + colored("doctor", "light_green") - + "\t\tDoctor mode shows available tools.\n" - ) - return epilogue + """Create the progress char legend for the help epilogue.""" + console = Console(record=True, force_terminal=True, no_color=False) + console.begin_capture() + console.print("[bold]Progress char key:[/bold]") + for kind, label in CHAR_KEY_LABELS: + mark = MARKS[kind] + console.print(f"\t[{mark.style}]{mark.char}[/{mark.style}] {label}") + console.print() + console.print("[bold blue]doctor mode:[/bold blue]") + doctor_mode = ( + f" [bright_magenta]{PROGRAM_NAME}[/bright_magenta] " + "[bright_green]doctor[/bright_green]\t\tDoctor mode shows available tools." + ) + console.print(doctor_mode) + return console.end_capture() def get_arguments(params: tuple[str, ...] | None = None) -> Namespace: @@ -381,19 +386,20 @@ def main(args: tuple[str, ...] | None = None) -> None: # the existing CLI into subparsers. PicoptDoctor.parse_cli() - printer = Printer(2) + setup_logging(2) try: arguments = get_arguments(args) - config = PicoptConfig(printer).get_config(arguments) + setup_logging(arguments.picopt.verbose) + config = PicoptConfig().get_config(arguments) walker = Walk(config) walker.walk() except ConfigError as err: - printer.error("", err) + logger.error(str(err)) sys.exit(78) except PicoptError as err: - printer.error("", err) + logger.error(str(err)) sys.exit(1) except Exception as exc: - printer.error("", exc) + logger.error(str(exc)) traceback.print_exception(exc) sys.exit(1) diff --git a/picopt/config/__init__.py b/picopt/config/__init__.py index 2936cf7a..02083ffa 100644 --- a/picopt/config/__init__.py +++ b/picopt/config/__init__.py @@ -26,6 +26,7 @@ Path as ConfusePath, ) from dateutil.parser import parse +from loguru import logger from picopt import PROGRAM_NAME from picopt import plugins as registry @@ -114,7 +115,7 @@ def _set_after(self, config: Subview) -> None: timestamp = time.mktime(after_dt.timetuple()) config["after"].set(timestamp) after = time.ctime(timestamp) - self._printer.config(f"Optimizing after {after}") + logger.info(f"Optimizing after {after}") @staticmethod def _get_ignore_regexp( @@ -138,8 +139,9 @@ def _get_ignore_regexp( ignore_single_stars.append(ignore_single_star) return r"|".join(ignore_regexps), ignore_single_stars + @staticmethod def _print_ignores( - self, ignore_single_stars: list[str], *, ignore_defaults: bool + ignore_single_stars: list[str], *, ignore_defaults: bool ) -> None: ignore_text = "" if ignore_single_stars: @@ -149,7 +151,7 @@ def _print_ignores( ignore_text += " " ignore_text += "Not ignoring dotfiles." if ignore_text: - self._printer.config(ignore_text) + logger.info(ignore_text) def _set_ignore(self, config: Subview) -> None: """Compute ignore regexp.""" @@ -188,7 +190,7 @@ def _set_timestamps(self, config: Subview) -> None: ts_str = f"Setting a timestamp file at the top of each directory tree: {roots_str}" else: ts_str = "Not setting timestamps." - self._printer.config(ts_str) + logger.info(ts_str) def get_config( self, args: Namespace | None = None, modname: str = PROGRAM_NAME diff --git a/picopt/config/handlers.py b/picopt/config/handlers.py index 7e22506f..f2904723 100644 --- a/picopt/config/handlers.py +++ b/picopt/config/handlers.py @@ -30,8 +30,9 @@ from typing import TYPE_CHECKING, Any +from loguru import logger + from picopt import plugins as registry -from picopt.printer import Printer if TYPE_CHECKING: from collections.abc import Iterable @@ -96,10 +97,6 @@ def _enabled_handler_classes( class ConfigHandlers: """Build the per-handler pipeline selection from the merged config.""" - def __init__(self, printer: Printer | None = None) -> None: - """Initialize printer.""" - self._printer: Printer = printer or Printer(2) - @staticmethod def _get_config_set(config: Subview, *keys: str) -> frozenset[str]: val_list: list[str] = [] @@ -108,8 +105,8 @@ def _get_config_set(config: Subview, *keys: str) -> frozenset[str]: val_list += config[key].get(list) or [] return frozenset(val.upper() for val in val_list) + @staticmethod def _print_formats_config( - self, verbose: int, handled_format_strs: set[str], convert_format_strs: dict[str, set[str]], @@ -117,12 +114,12 @@ def _print_formats_config( if not verbose: return handled_list = ", ".join(sorted(handled_format_strs)) - self._printer.config(f"Optimizing formats: {handled_list}") + logger.info(f"Optimizing formats: {handled_list}") for target, sources in convert_format_strs.items(): if not sources: continue from_list = ", ".join(sorted(sources)) - self._printer.config(f"Converting {from_list} to {target}") + logger.info(f"Converting {from_list} to {target}") def _set_format_handler_stages( self, diff --git a/picopt/doctor.py b/picopt/doctor.py index a62ad3e3..bf6549f1 100644 --- a/picopt/doctor.py +++ b/picopt/doctor.py @@ -15,9 +15,10 @@ import sys from typing import TYPE_CHECKING, Any -from termcolor import colored, cprint +from rich.markup import escape from picopt import plugins as registry +from picopt.log import console from picopt.plugins.webp import CWebPTool if TYPE_CHECKING: @@ -27,13 +28,15 @@ class PicoptDoctor: """Picopt doctor.""" - def __init__(self): + def __init__(self) -> None: """Init totals.""" self.total_required = 0 self.missing_required = 0 self.missing_optional = 0 - def _checkup_tool_get_tier_and_name(self, status, tier_idx: int, tool): + 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 @@ -51,93 +54,89 @@ def _checkup_tool_get_tier_and_name(self, status, tier_idx: int, tool): tier_color = "cyan" name = tool.name or type(tool).__name__ return [ - colored(f" tier {tier_idx} {prefix} {name}", tier_color) + f"[{tier_color}] tier {tier_idx} {prefix} {name}[/{tier_color}]" ], tier_has_available - def _checkup_tool(self, tier_idx, tool): + def _checkup_tool(self, tier_idx, tool) -> bool: status = tool.probe() bits, tier_has_available = self._checkup_tool_get_tier_and_name( - status, - tier_idx, - tool, + status, tier_idx, tool ) if status.version: - bits.append(colored(status.version, "black", attrs=["bold"])) + bits.append(f"[bold black]{escape(status.version)}[/bold black]") if status.path and status.path != "": - bits.append(colored(f"[{status.path}]", "white", attrs=["dark"])) + bits.append(f"[dim white]\\[{escape(status.path)}][/dim white]") if not status.available and status.error: - bits.extend(["-", colored(status.error, "red")]) + bits.extend(["-", f"[red]{escape(status.error)}[/red]"]) elif isinstance(tool, CWebPTool): # Probe-side-effect on WebPLossless: announce the cwebp generation. flag = "modern" if CWebPTool.IS_MODERN_CWEBP else "legacy" flag_color = "green" if CWebPTool.IS_MODERN_CWEBP else "cyan" - bits.append("".join(["(", colored(flag, flag_color), ")"])) + bits.append(f"([{flag_color}]{flag}[/{flag_color}])") - tool_report = " ".join(bits) - cprint(tool_report) + console.print(" ".join(bits)) return tier_has_available - def _checkup_handler_pipeline_tier(self, tier_idx: int, tier): + 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) if not tier_has_available: - cprint(f" !!! tier {tier_idx} has no available tool", "yellow") + console.print( + f"[yellow] !!! tier {tier_idx} has no available tool[/yellow]" + ) - def _checkup_handler(self, handler_cls): - cprint(f" {handler_cls.__name__}", "cyan", attrs=["bold"]) + def _checkup_handler(self, handler_cls) -> None: + console.print(f"[bold cyan] {handler_cls.__name__}[/bold cyan]") if not handler_cls.PIPELINE: - cprint(" (no external pipeline — always available)", "white") + console.print(" (no external pipeline — always available)") return for tier_idx, tier in enumerate(handler_cls.PIPELINE): self._checkup_handler_pipeline_tier(tier_idx, tier) - def _checkup_plugin(self, plugin): + def _checkup_plugin(self, plugin) -> None: if plugin.name == "PIL_CONVERTIBLE": return - cprint(plugin.name, "yellow") + console.print(f"[yellow]{plugin.name}[/yellow]") for handler_cls in plugin.handlers: self._checkup_handler(handler_cls) - def _checkup_report(self): + def _checkup_report(self) -> None: available_tools = self.total_required - self.missing_required required_color = "red" if self.missing_required else "green" - available_color = required_color optional_color = "cyan" if self.missing_optional else "green" - cprint( + summary = ( "Summary: " - + colored( - f"{available_tools}/{self.total_required} required tools available", - available_color, - ) - + ", " - + colored(f"{self.missing_required} missing required", required_color) - + ", " - + colored(f"{self.missing_optional} missing optional", optional_color) - + "." + f"[{required_color}]" + f"{available_tools}/{self.total_required} required tools 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}]." ) + console.print(summary) def checkup(self) -> int: """Run the doctor command. Returns a process exit code.""" - cprint( - colored("Plugins", "yellow") - + "\n " - + colored("Formats", "cyan", attrs=["bold"]) - + "\n " - + colored("Tools", "cyan") + header = ( + "[yellow]Plugins[/yellow]\n" + " [bold cyan]Formats[/bold cyan]\n" + " [cyan]Tools[/cyan]" ) + console.print(header) for plugin in sorted(registry.iter_plugins(), key=lambda p: p.name): self._checkup_plugin(plugin) - cprint("") + console.print("") self._checkup_report() return min(self.missing_required, 1) @classmethod - def doctor_mode(cls): + def doctor_mode(cls) -> None: """Create the doctor and perform a checkup.""" doctor = cls() sys.exit(doctor.checkup()) diff --git a/picopt/log/__init__.py b/picopt/log/__init__.py new file mode 100644 index 00000000..40e4b25c --- /dev/null +++ b/picopt/log/__init__.py @@ -0,0 +1,73 @@ +"""Loguru + Rich logging setup for picopt.""" + +from __future__ import annotations + +from contextlib import suppress +from typing import TYPE_CHECKING, Final + +from loguru import logger +from rich.console import Console + +from picopt.log.styles import LEVEL_STYLES + +if TYPE_CHECKING: + from loguru import Record + +__all__ = ("console", "logger", "setup") + +# Single Console for everything — both Rich Progress and the loguru sink +# share it so the live region and log lines stay in sync. +# +# `highlight=False` is critical: the default repr-highlighter would +# otherwise be applied to anything Rich re-prints internally, including +# the rendered ANSI strings produced by the live progress bar. On some +# installs that turns each `[` in `\x1b[Xm` sequences into a bold-styled +# bracket and leaves the leading `\x1b` as a stray byte, so the dots +# show up as literal `[2m[90m.[0m` text in the terminal. +console: Final[Console] = Console(highlight=False) + + +# verbose -> minimum loguru level to emit. Picopt's default verbose is 1, +# and the old termcolor Printer showed config-style messages +# (force_verbose=True) at that level — so INFO must be visible at verbose=1 +# or those regress to silent. +_VERBOSE_LEVEL: Final = { + 0: "ERROR", + 1: "INFO", +} + + +def _verbose_to_level(verbose: int) -> str: + if verbose <= 0: + return "ERROR" + return _VERBOSE_LEVEL.get(verbose, "DEBUG") + + +def _sink(message: object) -> None: + """Write a loguru record to the shared Rich console.""" + record: Record = message.record # pyright: ignore[reportAttributeAccessIssue],# ty: ignore[unresolved-attribute] + level = record["level"].name + style = LEVEL_STYLES.get(level, "white") + text = record["message"] + console.print(f"[{style}]{text}[/{style}]", highlight=False, soft_wrap=True) + + +_configured = False + + +def setup(verbose: int) -> None: + """Configure loguru for the given verbosity. Idempotent.""" + global _configured # noqa: PLW0603 + if not _configured: + # Already registered raises ValueError (e.g. test re-entry). + with suppress(ValueError): + logger.level("CONFIG", no=22, color="") + _configured = True + + logger.remove() + if verbose > 0: + logger.add( + _sink, + level=_verbose_to_level(verbose), + format="{message}", + ) diff --git a/picopt/log/progress.py b/picopt/log/progress.py new file mode 100644 index 00000000..348523f4 --- /dev/null +++ b/picopt/log/progress.py @@ -0,0 +1,208 @@ +"""Rich Progress bar with a streaming per-file char column.""" + +from __future__ import annotations + +import threading +from collections import defaultdict, deque +from typing import TYPE_CHECKING, Final + +from rich.progress import ( + MofNCompleteColumn, + Progress, + ProgressColumn, + SpinnerColumn, + TaskID, + TextColumn, + TimeElapsedColumn, +) +from rich.text import Text +from typing_extensions import Self, override + +from picopt.log.styles import MARKS + +if TYPE_CHECKING: + from types import TracebackType + + from rich.console import Console + from rich.progress import Task + +__all__ = ( + "CharStreamColumn", + "ProgressContext", + "make_progress", +) + + +# Marks that count as a finished file and advance the bar. +_FILE_MARKS: Final = frozenset( + { + "skipped", + "skipped_timestamp", + "copied", + "lost", + "dry_run", + "saved", + "converted", + "consumed_timestamp", + "error", + } +) + + +class CharStreamColumn(ProgressColumn): + """A column that shows the most-recent action chars as a streaming Text.""" + + def __init__(self, max_width: int = 40) -> None: + """Initialize the deque ring per task.""" + super().__init__() + self._max_width = max_width + self._streams: dict[int, deque[tuple[str, str]]] = defaultdict( + lambda: deque(maxlen=self._max_width) + ) + self._lock = threading.Lock() + + def push(self, task_id: int, char: str, style: str) -> None: + """Append a styled char to ``task_id``'s ring.""" + with self._lock: + self._streams[task_id].append((char, style)) + + @override + def render(self, task: Task) -> Text: + """Render the ring for ``task`` as a Rich Text.""" + text = Text() + with self._lock: + stream = list(self._streams.get(task.id, ())) + for char, style in stream: + text.append(char, style=style) + return text + + +class ProgressContext: + """ + Owns the Progress and the single TaskID; provides mark_* helpers. + + When ``enabled=False`` (no TTY, ``--quiet``, or unit tests) every + mark_*/__enter__/__exit__ is a no-op so callers can hold a + ProgressContext unconditionally. + """ + + def __init__( + self, + progress: Progress | None = None, + char_column: CharStreamColumn | None = None, + task_id: TaskID | None = None, + *, + enabled: bool = False, + ) -> None: + """Initialize.""" + self._progress = progress + self._char_column = char_column + self._task_id: TaskID | None = task_id + self._enabled = enabled + + def __enter__(self) -> Self: + """Enter the underlying live progress region (no-op when disabled).""" + if self._enabled and self._progress is not None: + self._progress.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Exit the underlying live progress region (no-op when disabled).""" + if self._enabled and self._progress is not None: + self._progress.__exit__(exc_type, exc_val, exc_tb) + + def _mark(self, kind: str) -> None: + if ( + not self._enabled + or self._progress is None + or self._char_column is None + or self._task_id is None + ): + return + mark = MARKS[kind] + self._char_column.push(int(self._task_id), mark.char, mark.style) + if kind in _FILE_MARKS: + self._progress.advance(self._task_id, 1) + + def mark_skipped(self) -> None: + """Mark a file as skipped (ignored, not handled, etc.).""" + self._mark("skipped") + + def mark_skipped_timestamp(self) -> None: + """Mark a file as skipped because its timestamp is older than recorded.""" + self._mark("skipped_timestamp") + + def mark_copied(self) -> None: + """Mark archive contents copied through unchanged.""" + self._mark("copied") + + def mark_lost(self) -> None: + """Mark a file whose optimized result was bigger than the original.""" + self._mark("lost") + + def mark_dry_run(self) -> None: + """Mark a file as no-op on dry run.""" + self._mark("dry_run") + + def mark_saved(self) -> None: + """Mark a successfully optimized file (smaller than original).""" + self._mark("saved") + + def mark_converted(self) -> None: + """Mark a successfully converted file.""" + self._mark("converted") + + def mark_packed(self) -> None: + """Mark a file packed into an archive.""" + self._mark("packed") + + def mark_consumed_timestamp(self) -> None: + """Mark a timestamp consumed from inside an archive.""" + self._mark("consumed_timestamp") + + def mark_warning(self) -> None: + """Mark a non-fatal issue (no bar advance).""" + self._mark("warning") + + def mark_error(self) -> None: + """Mark a fatal error processing a file.""" + self._mark("error") + + +def make_progress( + console: Console, + *, + enabled: bool = True, + total: int | None = None, + description: str = "Optimizing", +) -> ProgressContext: + """Build a ProgressContext, or a no-op when disabled / not a terminal.""" + if not enabled or not console.is_terminal: + return ProgressContext(enabled=False) + + # Size the streaming-char column so the whole bar fits on one line. + # If the bar wraps, Rich's Live region flips into multi-line mode + # and emits `\n` per refresh — which scrolls each frame past instead + # of redrawing in place. + # + # Reserve ~46 chars for the other columns: + # spinner(~2) + " Optimizing "(12) + counts(~12) + time(~12) + + # inter-column spaces. Cap the stream at 40 on very wide terminals. + char_width = max(8, min(40, console.width - 46)) + char_column = CharStreamColumn(max_width=char_width) + progress = Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + char_column, + MofNCompleteColumn(), + TimeElapsedColumn(), + console=console, + transient=False, + ) + task_id = progress.add_task(description, total=total) + return ProgressContext(progress, char_column, task_id, enabled=True) diff --git a/picopt/log/reporter.py b/picopt/log/reporter.py new file mode 100644 index 00000000..795ca609 --- /dev/null +++ b/picopt/log/reporter.py @@ -0,0 +1,70 @@ +"""Bundles Stats + ProgressContext so the scheduler has a single sink.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from picopt.log import console +from picopt.log.progress import ProgressContext +from picopt.log.styles import MARKS +from picopt.log.summary import Stats + +if TYPE_CHECKING: + from picopt.report import ReportStats + +__all__ = ("Reporter",) + + +@dataclass(slots=True) +class Reporter: + """ + Aggregates run-level reporting sinks. + + Defaults give a no-op progress and a detached Stats instance so + callers can construct a Reporter without wiring the full run plumbing + (used in tests and pre-progress setup). + """ + + stats: Stats = field(default_factory=Stats) + progress: ProgressContext = field(default_factory=ProgressContext) + verbose: int = 0 + + def record_report(self, report: ReportStats) -> None: + """Record a finished file's outcome — log + count + advance.""" + if report.exc is not None: + self.stats.record_error(report.path, str(report.exc)) + self.progress.mark_error() + self._print_outcome(report.report_text(), "error") + return + + bytes_out = ( + report.bytes_out + if report.saved > 0 and not report.bigger + else report.bytes_in + ) + 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._print_outcome(report.report_text(), kind) + + @staticmethod + def _classify(report: ReportStats) -> str: + if report.test: + return "dry_run" + if report.saved > 0: + return "converted" if report.converted else "saved" + return "lost" + + def _print_outcome(self, text: str, kind: str) -> None: + if self.verbose < 2: # noqa: PLR2004 + return + style = MARKS[kind].style + console.print(f"[{style}]{text}[/{style}]", highlight=False, soft_wrap=True) diff --git a/picopt/log/styles.py b/picopt/log/styles.py new file mode 100644 index 00000000..34bf9421 --- /dev/null +++ b/picopt/log/styles.py @@ -0,0 +1,84 @@ +""" +Centralized color / style / char definitions for picopt output. + +Single source of truth for everything user-facing: the streaming-char +column on the progress bar, the loguru sink that writes log lines, the +end-of-run summary table, and the help-epilogue char-key legend. + +Style choices intentionally mirror the old termcolor-based ``Printer`` +so longtime users see the same colors for the same outcomes: + + termcolor name → Rich style + ---------------- ----------------- + dark_grey → bright_black + light_grey → white (ANSI 37 — the "dim" white slot) + white → bright_white (ANSI 97 — the bright white slot) + light_green → bright_green + light_red → bright_red + light_blue → bright_blue + light_cyan → bright_cyan + light_yellow → bright_yellow + green / cyan / magenta / yellow → unchanged +""" + +from __future__ import annotations + +from dataclasses import dataclass +from types import MappingProxyType +from typing import TYPE_CHECKING, Final + +if TYPE_CHECKING: + from collections.abc import Mapping + +__all__ = ( + "LEVEL_STYLES", + "MARKS", + "Mark", +) + + +@dataclass(frozen=True, slots=True) +class Mark: + """A single (char, Rich-style) pair for a per-event progress mark.""" + + char: str + style: str + + +# Per-outcome marks. Keys mirror the printer-method names they replace, +# so call sites read naturally (``progress.mark_saved()`` matches the old +# ``printer.saved(...)``). +MARKS: Final[Mapping[str, Mark]] = MappingProxyType( + { + # Per-file marks (advance the progress bar). + "skipped": Mark(".", "bright_black"), + "skipped_timestamp": Mark(".", "bright_green dim bold"), + "copied": Mark(".", "green"), + "lost": Mark(".", "bright_blue bold"), + "dry_run": Mark(".", "bright_black bold"), + "saved": Mark(".", "bright_white"), + "converted": Mark(".", "bright_cyan"), + "packed": Mark(".", "white"), + "consumed_timestamp": Mark(".", "magenta"), + "warning": Mark("!", "bright_yellow"), + "error": Mark("X", "bright_red"), + } +) + + +def _style(key: str) -> str: + return MARKS[key].style + + +# Loguru level → Rich style. Levels that correspond to a per-event mark +# share that mark's style so log lines and progress chars match. +LEVEL_STYLES: Final[Mapping[str, str]] = MappingProxyType( + { + "DEBUG": _style("skipped"), + "INFO": "cyan", + "SUCCESS": _style("saved"), + "WARNING": _style("warning"), + "ERROR": _style("error"), + "CRITICAL": _style("error"), + } +) diff --git a/picopt/log/summary.py b/picopt/log/summary.py new file mode 100644 index 00000000..c968d426 --- /dev/null +++ b/picopt/log/summary.py @@ -0,0 +1,179 @@ +"""End-of-run summary statistics and rendering.""" + +from __future__ import annotations + +import threading +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from humanize import naturalsize +from rich.table import Table + +from picopt.log.styles import MARKS + +if TYPE_CHECKING: + from pathlib import Path + + from rich.console import Console + +__all__ = ("Stats", "render") + + +@dataclass(slots=True) +class Stats: + """Thread-safe counters and itemized lists for the end-of-run summary.""" + + # Mode flags set once at construction so the summary table can hide + # rows that are irrelevant to this run. + timestamps_active: bool = False + dry_run_active: bool = False + + skipped: int = 0 + skipped_timestamp: int = 0 + copied: int = 0 + + saved: list[Path] = field(default_factory=list) + converted: list[Path] = field(default_factory=list) + lost: list[Path] = field(default_factory=list) + dry_run: list[Path] = field(default_factory=list) + warnings: list[tuple[Path | None, str]] = field(default_factory=list) + errors: list[tuple[Path | None, str]] = field(default_factory=list) + + bytes_in: int = 0 + bytes_out: int = 0 + + _lock: threading.Lock = field(default_factory=threading.Lock, repr=False) + + def record_skipped(self) -> None: + """Increment the skipped counter.""" + with self._lock: + self.skipped += 1 + + def record_skipped_timestamp(self) -> None: + """Increment the timestamp-skipped counter.""" + with self._lock: + self.skipped_timestamp += 1 + + def record_copied(self) -> None: + """Increment the archive-copy counter.""" + with self._lock: + self.copied += 1 + + def record_saved(self, path: Path) -> None: + """Append a successfully-optimized file path.""" + with self._lock: + self.saved.append(path) + + def record_converted(self, path: Path) -> None: + """Append a successfully-converted file path.""" + with self._lock: + self.converted.append(path) + + def record_lost(self, path: Path) -> None: + """Append a file whose optimization was discarded for being larger.""" + with self._lock: + self.lost.append(path) + + def record_dry_run(self, path: Path) -> None: + """Append a path that would have been optimized (dry-run).""" + with self._lock: + self.dry_run.append(path) + + def record_warning(self, path: Path | None, message: str) -> None: + """Append a warning tied to a file.""" + with self._lock: + self.warnings.append((path, message)) + + def record_error(self, path: Path | None, message: str) -> None: + """Append an error tied to a file.""" + with self._lock: + self.errors.append((path, message)) + + def record_bytes(self, bytes_in: int, bytes_out: int) -> None: + """Add to the run-level byte totals.""" + with self._lock: + self.bytes_in += bytes_in + self.bytes_out += bytes_out + + +def _counts_table(stats: Stats) -> Table: + """ + Build the Counts table for the summary. + + Row styles match the per-event color scheme used by the loguru sink + and the progress bar's CharStreamColumn so the same outcome reads + the same way everywhere. + """ + table = Table(title="Summary", show_header=False, title_style="bold") + table.add_column("Metric") + table.add_column("Count", justify="right") + table.add_row("Skipped", str(stats.skipped), style=MARKS["skipped"].style) + if stats.timestamps_active: + table.add_row( + "Skipped (timestamp)", + str(stats.skipped_timestamp), + style=MARKS["skipped_timestamp"].style, + ) + if stats.copied: + table.add_row( + "Copied unchanged", str(stats.copied), style=MARKS["copied"].style + ) + table.add_row("Saved", str(len(stats.saved)), style=MARKS["saved"].style) + if stats.converted: + table.add_row( + "Converted", str(len(stats.converted)), style=MARKS["converted"].style + ) + if stats.lost: + table.add_row("Lost", str(len(stats.lost)), style=MARKS["lost"].style) + if stats.dry_run_active: + table.add_row( + "Would optimize (dry run)", + str(len(stats.dry_run)), + style=MARKS["dry_run"].style, + ) + if stats.warnings: + table.add_row( + "Warnings", str(len(stats.warnings)), style=MARKS["warning"].style + ) + if stats.errors: + table.add_row("Errors", str(len(stats.errors)), style=MARKS["error"].style) + return table + + +def _print_pairs( + console: Console, + header: str, + pairs: list[tuple[Path | None, str]], + style: str = "", +) -> None: + if not pairs: + return + console.print(f"[bold]{header}:[/bold]") + for path, message in pairs: + line = f" - {path}: {message}" if path else f" - {message}" + console.print(f"[{style}]{line}[/{style}]" if style else line, highlight=False) + + +def _bytes_summary(stats: Stats, *, dry_run: bool) -> str: + if not stats.bytes_in: + return "Didn't optimize any files." + bytes_saved = stats.bytes_in - stats.bytes_out + percent_saved = bytes_saved / stats.bytes_in * 100 + sign = (percent_saved > 0) - (percent_saved < 0) + if dry_run: + verbs = {1: "Could save", 0: "Could even out for", -1: "Could lose"} + else: + verbs = {1: "Saved", 0: "Evened out", -1: "Lost"} + natural = naturalsize(abs(bytes_saved)) + return f"{verbs[sign]} a total of {natural} or {abs(percent_saved):.2f}%" + + +def render(stats: Stats, console: Console, *, dry_run: bool = False) -> None: + """Print the summary to the given Rich console.""" + console.print(_counts_table(stats)) + _print_pairs(console, "Warnings", stats.warnings, MARKS["warning"].style) + _print_pairs(console, "Errors", stats.errors, MARKS["error"].style) + summary_line = _bytes_summary(stats, dry_run=dry_run) + console.print(summary_line, highlight=False) + if dry_run: + console.print("Dry run did not change any files.", highlight=False) diff --git a/picopt/pillow/jpeg_xmp.py b/picopt/pillow/jpeg_xmp.py deleted file mode 100755 index 8a2d6a9d..00000000 --- a/picopt/pillow/jpeg_xmp.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python -"""Insert xmp xml into jpegs.""" - -import struct - -_APP1_SECTION_DELIMITER = b"\x00" -_XAP_MARKER = b"http://ns.adobe.com/xap/1.0/" -_SOI_MARKER = b"\xff\xd8" -_EOI_MARKER = b"\xff\xe1" - - -def set_jpeg_xmp(jpeg_data: bytes, xmp: str) -> bytes: - """Insert xmp data into jpeg.""" - jpeg_buffer = bytearray(jpeg_data) - soi_index = jpeg_buffer.find(_SOI_MARKER) - if soi_index == -1: - reason = "SOI marker not found in JPEG buffer." - raise ValueError(reason) - xmp_bytes = ( - _XAP_MARKER - + _APP1_SECTION_DELIMITER - + xmp.encode("utf-8") - + _APP1_SECTION_DELIMITER - ) - return ( - bytes(jpeg_buffer[: soi_index + len(_SOI_MARKER)]) - + _EOI_MARKER - + struct.pack(" None: @override def walk(self) -> Generator[PathInfo]: """Yield each frame as a child PathInfo.""" - self._printer.container_unpacking(self.path_info) + if self.config.verbose > 1: + logger.info(f"Unpacking {self.path_info.full_output_name()}…") frame_info: dict[str, Any] = {} index = 0 with Image.open(self.original_path) as image: @@ -154,7 +156,6 @@ def pack_into(self) -> BinaryIO: for path_info in sorted_frames[1:]: frame = Image.open(BytesIO(path_info.data())) append_images.append(frame) - self._printer.packed() info = dict(self.prepare_info(self.OUTPUT_FORMAT_STR)) info.update(self.frame_info) diff --git a/picopt/plugins/base/archive.py b/picopt/plugins/base/archive.py index ac65ae60..59356f49 100644 --- a/picopt/plugins/base/archive.py +++ b/picopt/plugins/base/archive.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any +from loguru import logger from typing_extensions import override from picopt.archiveinfo import ArchiveInfo @@ -94,10 +95,8 @@ def _archive_write(self, archive) -> None: while self._optimized_contents: path_info = self._optimized_contents.pop() self._pack_info_one_file(archive, path_info) - self._printer.packed() if self.comment: archive.comment = self.comment - self._printer.packed() @override def pack_into(self) -> BytesIO: @@ -169,8 +168,8 @@ 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: - self._printer.consumed_timestamp(path) + if self._skipper and self.config.verbose > 1: + logger.info(f"Consumed picopt timestamp in archive: {path}") self._do_repack = True return tuple(non_treestamp_entries) @@ -187,7 +186,8 @@ def _copy_unchanged_files(self, archive) -> None: @override def walk(self) -> Generator[PathInfo]: """Walk an archive's entries.""" - self._printer.scan_archive(self.path_info) + if self.config.verbose > 1: + logger.info(f"Scanning archive {self.path_info.full_output_name()}…") with self._get_archive() as archive: non_treestamp_entries = self._consume_archive_timestamps(archive) for archiveinfo in non_treestamp_entries: diff --git a/picopt/plugins/base/container.py b/picopt/plugins/base/container.py index c1a74faa..62b4ff91 100644 --- a/picopt/plugins/base/container.py +++ b/picopt/plugins/base/container.py @@ -26,8 +26,10 @@ from copy import copy from typing import TYPE_CHECKING, Any, BinaryIO +from loguru import logger from typing_extensions import override +from picopt.log.reporter import Reporter from picopt.plugins.base.handler import Handler if TYPE_CHECKING: @@ -65,8 +67,10 @@ def __init__( # 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, self._printer, timestamps, in_archive=True + self.config, Reporter(), timestamps, in_archive=True ) self.comment: bytes | None = comment self._optimized_contents: set[PathInfo] = optimized_contents or set() @@ -79,13 +83,16 @@ def walk(self) -> Generator[PathInfo]: """Yield each child PathInfo in the container.""" def _walk_finish(self) -> None: - if not self.config.verbose: + if self.config.verbose < 2: # noqa: PLR2004 return - self._printer.done() if self._do_repack and self._skipper: - self._printer.optimize_container(self.path_info) + logger.info(f"Optimizing contents in {self.path_info.full_output_name()}") else: - self._printer.skip_container(self.CONTAINER_TYPE, self.path_info) + msg = ( + f"Skip: {self.CONTAINER_TYPE}, contents skipped. " + f"Optimizing during repack: {self.path_info.full_output_name()}" + ) + logger.info(msg) # ----------------------------------------------------- task accumulation @@ -135,10 +142,9 @@ def optimize(self) -> BinaryIO: if not self.CAN_PACK: msg = f"{type(self).__name__} cannot optimize a non-packing container." raise NotImplementedError(msg) - self._printer.container_repacking(self.path_info) - buffer = self.pack_into() - self._printer.container_repacking_done() - return buffer + if self.config.verbose > 1: + logger.info(f"Repacking {self.path_info.full_output_name()}…") + return self.pack_into() def __getstate__(self) -> dict[str, Any]: """Drop Grovestamps for worker handoff; its ruamel.yaml Reader owns an un-picklable BufferedReader.""" diff --git a/picopt/plugins/base/handler.py b/picopt/plugins/base/handler.py index fb94f47a..cf088843 100644 --- a/picopt/plugins/base/handler.py +++ b/picopt/plugins/base/handler.py @@ -28,7 +28,6 @@ from picopt import WORKING_SUFFIX from picopt.path import DOUBLE_SUFFIX, PathInfo from picopt.plugins.base.format import FileFormat -from picopt.printer import Printer from picopt.report import ReportStats if TYPE_CHECKING: @@ -77,7 +76,6 @@ def __init__( """Initialize handler state.""" self.config: AttrDict = config self.path_info: PathInfo = path_info - self._printer: Printer = Printer(self.config.verbose) # Paths self.original_path: Path = path_info.path or Path(path_info.name()) @@ -232,13 +230,10 @@ def optimize_wrapper(self) -> ReportStats: """Run optimize() and convert the result into a ReportStats record.""" try: buffer = self.optimize() - report_stats = self._cleanup_after_optimize(buffer) + return self._cleanup_after_optimize(buffer) except Exception as exc: traceback.print_exc() - report_stats = self.error(exc) - if self.config.verbose: - report_stats.report(self._printer) - return report_stats + return self.error(exc) # --------------------------------------------------------------- cleanup diff --git a/picopt/plugins/base/image.py b/picopt/plugins/base/image.py index d34f08ca..262cc082 100644 --- a/picopt/plugins/base/image.py +++ b/picopt/plugins/base/image.py @@ -13,6 +13,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING, Any, BinaryIO +from loguru import logger from PIL import Image from PIL.PngImagePlugin import PngImageFile, PngInfo from PIL.WebPImagePlugin import WebPImageFile @@ -124,12 +125,11 @@ def optimize(self) -> BinaryIO: """Run each pipeline stage in sequence.""" stages = self.selected_stages() if not stages: - self._printer.warn( - ( - f"Tried to execute handler {type(self).__name__} with no " - "available pipeline stages." - ), + msg = ( + f"Tried to execute handler {type(self).__name__} with no " + "available pipeline stages." ) + logger.warning(msg) msg = f"No pipeline stages available for {type(self).__name__}" raise ValueError(msg) buf: BinaryIO = self.path_info.fp_or_buffer() diff --git a/picopt/plugins/jpeg.py b/picopt/plugins/jpeg.py index 3c7ec0b3..8eefe422 100644 --- a/picopt/plugins/jpeg.py +++ b/picopt/plugins/jpeg.py @@ -8,14 +8,15 @@ from __future__ import annotations +import struct from io import BytesIO from typing import BinaryIO +from loguru import logger from PIL.JpegImagePlugin import JpegImageFile from PIL.MpoImagePlugin import MpoImageFile from typing_extensions import override -from picopt.pillow.jpeg_xmp import set_jpeg_xmp from picopt.plugins.base import ( Handler, ImageHandler, @@ -31,6 +32,10 @@ _MPO_METADATA: int = 45058 _MPO_TYPE_PRIMARY: str = "Baseline MP Primary Image" +_APP1_SECTION_DELIMITER = b"\x00" +_XAP_MARKER = b"http://ns.adobe.com/xap/1.0/" +_SOI_MARKER = b"\xff\xd8" +_EOI_MARKER = b"\xff\xe1" # --------------------------------------------------------------------------- @@ -38,6 +43,28 @@ # --------------------------------------------------------------------------- +def set_jpeg_xmp(jpeg_data: bytes, xmp: str) -> bytes: + """Insert xmp data into jpeg.""" + jpeg_buffer = bytearray(jpeg_data) + soi_index = jpeg_buffer.find(_SOI_MARKER) + if soi_index == -1: + reason = "SOI marker not found in JPEG buffer." + raise ValueError(reason) + xmp_bytes = ( + _XAP_MARKER + + _APP1_SECTION_DELIMITER + + xmp.encode("utf-8") + + _APP1_SECTION_DELIMITER + ) + return ( + bytes(jpeg_buffer[: soi_index + len(_SOI_MARKER)]) + + _EOI_MARKER + + struct.pack(" BinaryIO: try: jpeg_data = self._copy_exif(handler, jpeg_data) except Exception as exc: - handler._printer.warn( # noqa: SLF001 - f"could not copy EXIF data for {handler.path_info.full_output_name()}", - exc, + msg = ( + f"could not copy EXIF data for " + f"{handler.path_info.full_output_name()}: {exc}" ) + logger.warning(msg) try: jpeg_data = self._copy_xmp(handler, jpeg_data) except Exception as exc: - handler._printer.warn( # noqa: SLF001 - f"could not copy XMP data for {handler.path_info.full_output_name()}", - exc, + msg = ( + f"could not copy XMP data for " + f"{handler.path_info.full_output_name()}: {exc}" ) + logger.warning(msg) handler.input_file_format = handler.OUTPUT_FILE_FORMAT return BytesIO(jpeg_data) diff --git a/picopt/plugins/pdf.py b/picopt/plugins/pdf.py index 6380cef8..ba3fef05 100644 --- a/picopt/plugins/pdf.py +++ b/picopt/plugins/pdf.py @@ -66,8 +66,8 @@ from typing import TYPE_CHECKING, Any from zipfile import ZipInfo +from loguru import logger from pikepdf.exceptions import PasswordError -from termcolor import cprint from typing_extensions import override from picopt.path import PathInfo @@ -317,8 +317,7 @@ def _walk_pdf_obj(self, obj, pikepdf) -> PathInfo | None: raw = obj.read_raw_bytes() except Exception: # One weird object should never sink the whole file. - if self.config.verbose > 1: - cprint(f"Read error on PDF object {obj}, continuing.", "yellow") + logger.warning(f"Read error on PDF object {obj}, continuing.") return None if not raw: return None @@ -332,7 +331,8 @@ def walk(self) -> Generator[PathInfo]: Non-DCT streams are not yielded — qpdf will recompress them structurally during pack_into(), no per-child handler needed. """ - self._printer.scan_archive(self.path_info) + if self.config.verbose > 1: + logger.info(f"Scanning archive {self.path_info.full_output_name()}…") try: pdf = self._open_input_pdf() except Exception as exc: @@ -348,7 +348,7 @@ def walk(self) -> Generator[PathInfo]: f"{self.path_info.full_output_name()}: " "PDF has a digital signature; refusing to modify." ) - cprint(msg, "yellow") + logger.warning(msg) else: import pikepdf @@ -361,7 +361,7 @@ def walk(self) -> Generator[PathInfo]: f"{self.path_info.full_output_name()}: " "PDF is encrypted; refusing to modify." ) - cprint(msg, "yellow") + logger.warning(msg) finally: with suppress(Exception): pdf.close() @@ -393,14 +393,14 @@ def _apply_optimized_jpeg(self, child, objgen_to_obj, pikepdf) -> int: original_raw = obj.read_raw_bytes() optimized = child.data() except Exception: - cprint(f"Error reading PDF image: {obj}", "red") + logger.error(f"Error reading PDF image: {obj}") raise if not optimized or len(optimized) >= len(original_raw): return 0 try: obj.write(optimized, filter=pikepdf.Name.DCTDecode) except Exception: - cprint(f"Error writing optimized PDF image {obj}", "red") + logger.error(f"Error writing optimized PDF image {obj}") raise return 1 diff --git a/picopt/plugins/png.py b/picopt/plugins/png.py index 45c0a785..c7a30373 100644 --- a/picopt/plugins/png.py +++ b/picopt/plugins/png.py @@ -12,6 +12,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING, Any +from loguru import logger from PIL.PngImagePlugin import PngImageFile from typing_extensions import override @@ -90,10 +91,14 @@ def run_stage(self, handler: Handler, buf: BinaryIO) -> BinaryIO: try: depth = png_bit_depth(buf) except ValueError as exc: - handler._printer.warn(str(exc)) # noqa: SLF001 + logger.warning(str(exc)) return buf if not depth or depth > _PNGOUT_DEPTH_MAX or depth < 1: - handler._printer.skip(f"pngout for {depth} bit PNG", handler.path_info) # noqa: SLF001 + msg = ( + f"Skip: pngout for {depth} bit PNG: " + f"{handler.path_info.full_output_name()}" + ) + logger.debug(msg) return buf keep_arg = ("-k1",) if handler.config.keep_metadata else ("-k0",) return self.run_ext((*self.exec_args(), *_PNGOUT_ARGS, *keep_arg), buf) diff --git a/picopt/plugins/webp.py b/picopt/plugins/webp.py index a9f91fb2..0c0aef55 100644 --- a/picopt/plugins/webp.py +++ b/picopt/plugins/webp.py @@ -69,6 +69,7 @@ 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 @@ -598,7 +599,8 @@ def _read_durations(self, num_frames: int) -> dict[int, int]: @override def walk(self) -> Generator[PathInfo]: - self._printer.container_unpacking(self.path_info) + 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() diff --git a/picopt/printer.py b/picopt/printer.py deleted file mode 100644 index cc514892..00000000 --- a/picopt/printer.py +++ /dev/null @@ -1,160 +0,0 @@ -"""Print Messages.""" - -from pathlib import Path -from traceback import print_exception - -from termcolor import cprint - -from picopt.path import PathInfo - - -class Printer: - """Printing messages during walk and handling.""" - - def __init__(self, verbose: int) -> None: - """Initialize verbosity and flags.""" - self._verbose: int = verbose - self._after_newline: bool = True - - def _message( - self, - message: str, - color: str = "white", - attrs: list[str] | None = None, - end: str = "\n", - *, - force_verbose: bool = False, - force_continue_line: bool = False, - ) -> None: - """Print a dot or skip message.""" - if self._verbose < 1: - return - if not message: - reason = "No message given to printer" - raise ValueError(reason) - if self._verbose == 1 and not force_verbose: - message = "." - end = "" - elif not self._after_newline and not force_continue_line: - message = "\n" + message - attrs = attrs or [] - cprint(message, color, attrs=attrs, end=end, flush=True) - self._after_newline = bool(end) - - def config(self, message: str) -> None: - """Config messages.""" - self._message(message, color="cyan", force_verbose=True) - - def skip( - self, - reason: str, - path_info: PathInfo, - color: str = "dark_grey", - attrs: list[str] | None = None, - ) -> None: - """Skip Message.""" - parts = ("Skip", reason, path_info.full_output_name()) - message = ": ".join(parts) - self._message(message, color=color, attrs=attrs) - - def skip_container(self, container_type: str, path_info: PathInfo) -> None: - """Skip entire container.""" - reason = f"{container_type}, contents skipped. Optimizing during repack." - self.skip(reason, path_info) - - def skip_timestamp(self, message: str, path_info: PathInfo) -> None: - """Skipped by timestamp.""" - self.skip(message, path_info, color="light_green", attrs=["dark", "bold"]) - - def start_operation( - self, operation: str, path_info: PathInfo, *, force_newline: bool = True - ) -> None: - """Scan archive start.""" - path = path_info.full_output_name() - if self._verbose == 1 and force_newline: - self._after_newline = False - self._message(f"{operation} {path}...", force_verbose=True, end="") - - def deleted(self, path: Path | str) -> None: - """Print deleted message.""" - self._message(f"Deleted {path}", color="yellow") - - def consumed_timestamp(self, path: Path | str) -> None: - """Consume timestamp message.""" - self._message(f"Consumed picopt timestamp in archive: {path}", color="magenta") - - def scan_archive(self, path_info: PathInfo) -> None: - """Scan archive start.""" - self.start_operation("Scanning archive", path_info) - - def container_unpacking(self, path_info: PathInfo) -> None: - """Start Unpacking Operation.""" - # this fixes containers within containers newlines. - self._after_newline = False - self.start_operation("Unpacking", path_info) - - def container_repacking(self, path_info: PathInfo) -> None: - """Start Repacking Operation.""" - if self._verbose > 1: - self.start_operation("Repacking", path_info) - - def img2webp_repacking(self, path_info: PathInfo) -> None: - """Start img2webp Repacking Operation.""" - if self._verbose > 1: - self.start_operation( - "Optimizing while repacking animated WebP...", path_info - ) - - def container_repacking_done(self) -> None: - """Only done for repack if very verbose.""" - if self._verbose > 1: - self.done() - - def copied(self) -> None: - """Dot for copied file.""" - self._message(".", color="green", end="", force_continue_line=True) - - def optimize_container(self, path_info: PathInfo) -> None: - """Declare that we're optimizing contents.""" - self.start_operation("Optimizing contents in", path_info, force_newline=False) - - def packed(self) -> None: - """Dot for repacked file.""" - self._message(".", color="light_grey", end="", force_continue_line=True) - - def done(self) -> None: - """Operation done.""" - self._message("done.", force_verbose=True, force_continue_line=True) - - def saved(self, report: str) -> None: - """Report saved size.""" - self._message(report) - - def converted(self, report: str) -> None: - """Report converted file.""" - self._message(report, color="light_cyan") - - def lost(self, report: str) -> None: - """Lost size.""" - self._message(report, color="light_blue") - - def warn(self, message: str, exc: BaseException | None = None) -> None: - """Warning.""" - message = "WARNING: " + message - if exc: - message += f": {exc}" - self._message(message, color="light_yellow", force_verbose=True) - - def error(self, message: str, exc: BaseException) -> None: - """Error.""" - message = "ERROR: " + message + f": {exc}" - self._message(message, color="light_red", force_verbose=True) - print_exception(exc) - - def error_title(self, message: str) -> None: - """Error title.""" - self._message(message, color="light_red", force_verbose=True) - - def final_message(self, message: str) -> None: - """Print final message.""" - self._message(message, force_verbose=True) diff --git a/picopt/report.py b/picopt/report.py index 7bcba378..bab8d5be 100644 --- a/picopt/report.py +++ b/picopt/report.py @@ -1,13 +1,18 @@ -"""Statistics for the optimization operations.""" +"""Per-file optimization result record.""" + +from __future__ import annotations -from pathlib import Path from subprocess import CalledProcessError +from typing import TYPE_CHECKING -from confuse import AttrDict from humanize import naturalsize -from picopt.path import PathInfo -from picopt.printer import Printer +if TYPE_CHECKING: + from pathlib import Path + + from confuse import AttrDict + + from picopt.path import PathInfo class ReportStats: @@ -82,70 +87,6 @@ def _report_error(self) -> str: report += f"\n{self._TAB}{self.exc!s}" return report - def report(self, printer: Printer) -> None: - """Record the percent saved & print it.""" - # Pass the printer in at the end here to avoid pickling - if self.exc: - report = self._report_error() - printer.error(report, self.exc) - return - report = self._report_saved() - if self.saved > 0: - if self.converted: - printer.converted(report) - else: - printer.saved(report) - else: - printer.lost(report) - - -class Totals: - """Totals for final report.""" - - def __init__(self, config: AttrDict, printer: Printer) -> None: - """Initialize Totals.""" - self.bytes_in: int = 0 - self.bytes_out: int = 0 - self.errors: list[ReportStats] = [] - self._config: AttrDict = config - self._printer: Printer = printer - - def _report_bytes_in(self) -> None: - """Report Totals if there were bytes in.""" - if not self._config.verbose and not self._config.dry_run: - return - bytes_saved = self.bytes_in - self.bytes_out - percent_bytes_saved = bytes_saved / self.bytes_in * 100 - sign = (percent_bytes_saved > 0) - (percent_bytes_saved < 0) - match (bool(self._config.dry_run), sign): - case (True, 1): - msg = "Could save" - case (True, 0): - msg = "Could even out for" - case (True, -1): - msg = "Could lose" - case (False, 1): - msg = "Saved" - case (False, 0): - msg = "Evened out" - case _: - msg = "Lost" - natural_saved = naturalsize(bytes_saved) - msg += f" a total of {natural_saved} or {percent_bytes_saved:.2f}%" - self._printer.saved(msg) - if self._config.dry_run: - self._printer.final_message("Dry run did not change any files.") - - def report(self) -> None: - """Report the total number and percent of bytes saved.""" - if self._config.verbose == 1: - print() # noqa: T201 - if self.bytes_in: - self._report_bytes_in() - else: - self._printer.final_message("Didn't optimize any files.") - - if self.errors: - self._printer.error_title("Errors with the following files:") - for rs in self.errors: - rs.report(self._printer) + def report_text(self) -> str: + """Return the human-readable line for this report.""" + return self._report_error() if self.exc else self._report_saved() diff --git a/picopt/walk/handler_factory.py b/picopt/walk/handler_factory.py index f04b8b97..f204b7db 100644 --- a/picopt/walk/handler_factory.py +++ b/picopt/walk/handler_factory.py @@ -23,37 +23,44 @@ PackingArchiveHandler)`` check. """ -from collections.abc import Mapping +from __future__ import annotations + from traceback import print_exc -from typing import Any +from typing import TYPE_CHECKING, Any -from confuse.templates import AttrDict -from treestamps import Grovestamps +from loguru import logger from picopt import plugins as registry -from picopt.path import PathInfo from picopt.plugins.base import ( ArchiveHandler, ContainerHandler, Handler, ImageHandler, ) -from picopt.plugins.base.format import FileFormat -from picopt.printer import Printer from picopt.walk.detect_format import detect_format +if TYPE_CHECKING: + from collections.abc import Mapping + + from confuse.templates import AttrDict + from treestamps import Grovestamps + + from picopt.log.reporter import Reporter + from picopt.path import PathInfo + from picopt.plugins.base.format import FileFormat + class HandlerFactory: """Handler factory for creating format-appropriate handlers.""" - def __init__(self, config: AttrDict, printer: Printer) -> None: - """Initialize with config and printer.""" + def __init__(self, config: AttrDict, reporter: Reporter) -> None: + """Initialize with config and reporter.""" self._config: AttrDict = config - self._printer: Printer = printer + self._reporter: Reporter = reporter def _lookup_route( self, - file_format: "FileFormat | None", + file_format: FileFormat | None, ) -> tuple | None: if not file_format: return None @@ -165,10 +172,11 @@ def _get_repack_handler_class( if picked is not None and issubclass(picked, ContainerHandler): repack_handler_class = picked except OSError as exc: - self._printer.warn( - f"getting repack container handler for {path_info.full_output_name()}", - exc, + msg = ( + f"getting repack container handler for " + f"{path_info.full_output_name()}: {exc}" ) + logger.warning(msg) print_exc() if ( @@ -177,9 +185,11 @@ def _get_repack_handler_class( and not self._config.list_only ): fmt = str(file_format) if file_format else "unknown" - self._printer.skip( - f"({fmt}) is not an enabled image or container format.", path_info + msg = ( + f"Skip: ({fmt}) is not an enabled image or container format: " + f"{path_info.full_output_name()}" ) + logger.debug(msg) return repack_handler_class @@ -196,9 +206,7 @@ def _create_handler_get_class_and_format( convert=path_info.convert, ) except OSError as exc: - self._printer.warn( - f"getting handler for {path_info.full_output_name()}", exc - ) + logger.warning(f"getting handler for {path_info.full_output_name()}: {exc}") print_exc() file_format = None info = {} diff --git a/picopt/walk/scheduler.py b/picopt/walk/scheduler.py index cfd7df4f..23f44a22 100644 --- a/picopt/walk/scheduler.py +++ b/picopt/walk/scheduler.py @@ -43,10 +43,9 @@ from confuse.templates import AttrDict from treestamps import Grovestamps + from picopt.log.reporter import Reporter from picopt.path import PathInfo from picopt.plugins.base import ContainerHandler, ImageHandler - from picopt.printer import Printer - from picopt.report import Totals # --------------------------------------------------------------------- state @@ -217,8 +216,7 @@ def __init__( config: AttrDict, executor: ProcessPoolExecutor, timestamps: Grovestamps | None, - totals: Totals, - printer: Printer, + reporter: Reporter, max_workers: int, create_repack_handler: Callable[[AttrDict, ContainerHandler], ContainerHandler], child_enqueue_callback: Callable[ @@ -229,8 +227,7 @@ def __init__( self._config = config self._executor = executor self._timestamps = timestamps - self._totals = totals - self._printer = printer + self._reporter = reporter self._max_workers = max_workers self._create_repack_handler = create_repack_handler self._child_enqueue_callback = child_enqueue_callback @@ -483,8 +480,7 @@ def _handle_leaf_done(self, entry: _LeafEntry, report: ReportStats) -> None: parent.had_work = True else: # leaf error inside a container — record, keep going - self._totals.errors.append(report) - report.report(self._printer) + self._reporter.record_report(report) parent.pending = max(0, parent.pending - 1) self._maybe_start_repack(parent) return @@ -497,8 +493,7 @@ def _handle_leaf_done(self, entry: _LeafEntry, report: ReportStats) -> None: def _handle_repack_failure(self, report: ReportStats, node: ContainerNode) -> None: if self._config.fail_fast: - self._totals.errors.append(report) - report.report(self._printer) + self._reporter.record_report(report) self._trigger_fail_fast(report.exc) return if self._config.fail_fast_container: @@ -507,14 +502,12 @@ def _handle_repack_failure(self, report: ReportStats, node: ContainerNode) -> No while root.parent is not None: root = root.parent self._cancel_subtree(root, reason=report.exc) - self._totals.errors.append(report) - report.report(self._printer) + self._reporter.record_report(report) 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._totals.errors.append(report) - report.report(self._printer) + 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) @@ -592,16 +585,8 @@ def _maybe_start_repack(self, node: ContainerNode) -> None: self._ready.append((RepackJob(handler=repack_handler), node)) def _record_totals(self, report: ReportStats) -> None: - """Accumulate one ReportStats into Totals.""" - if report.exc: - self._totals.errors.append(report) - report.report(self._printer) - return - self._totals.bytes_in += report.bytes_in - if report.saved > 0 and not self._config.bigger: - self._totals.bytes_out += report.bytes_out - else: - self._totals.bytes_out += report.bytes_in + """Hand one ReportStats off to the Reporter for stats + progress + log.""" + self._reporter.record_report(report) def _write_timestamp(self, report: ReportStats, top_path: Path) -> None: """Write a timestamp if timestamps are enabled and no error.""" diff --git a/picopt/walk/skip.py b/picopt/walk/skip.py index 2512f230..17665203 100644 --- a/picopt/walk/skip.py +++ b/picopt/walk/skip.py @@ -1,16 +1,25 @@ """Walk Methods for checking and skipping.""" +from __future__ import annotations + import shutil -from pathlib import Path +from typing import TYPE_CHECKING -from confuse import AttrDict -from treestamps import Grovestamps, Treestamps +from loguru import logger +from treestamps import Treestamps from picopt import PROGRAM_NAME, WORKING_SUFFIX from picopt.path import PathInfo, is_path_ignored -from picopt.printer import Printer from picopt.walk.legacy_timestamps import OLD_TIMESTAMPS_NAME +if TYPE_CHECKING: + from pathlib import Path + + from confuse import AttrDict + from treestamps import Grovestamps + + from picopt.log.reporter import Reporter + class WalkSkipper: """Walk Methods for checking and skipping.""" @@ -22,7 +31,7 @@ class WalkSkipper: def __init__( self, config: AttrDict, - printer: Printer, + reporter: Reporter, timestamps: Grovestamps | None = None, *, in_archive: bool = False, @@ -31,7 +40,7 @@ def __init__( self._config: AttrDict = config self._timestamps: Grovestamps | None = timestamps self._in_archive: bool = in_archive - self._printer: Printer = printer + self._reporter: Reporter = reporter def set_timestamps(self, timestamps: Grovestamps) -> None: """Reset the timestamps after they've been established.""" @@ -41,9 +50,13 @@ def _log_skip(self, reason: str, path_info: PathInfo, *, warn: bool) -> None: if not reason: return if warn: - self._printer.warn(reason) - else: - self._printer.skip(reason, path_info) + logger.warning(reason) + self._reporter.stats.record_warning(path_info.path, reason) + self._reporter.progress.mark_warning() + return + logger.debug(f"Skip: {reason}: {path_info.full_output_name()}") + self._reporter.stats.record_skipped() + self._reporter.progress.mark_skipped() def _is_skippable(self, path_info: PathInfo) -> bool: """Handle things that are not optimizable files.""" @@ -80,9 +93,11 @@ def _clean_up_working_files(self, path: Path) -> None: shutil.rmtree(path, ignore_errors=True) else: path.unlink(missing_ok=True) - self._printer.deleted(path) + logger.info(f"Deleted {path}") except Exception as exc: - self._printer.error("", exc) + logger.error(f"Could not delete {path}: {exc}") + self._reporter.stats.record_error(path, str(exc)) + self._reporter.progress.mark_error() def is_walk_file_skip( self, @@ -114,5 +129,7 @@ def is_older_than_timestamp( mtime = path_info.mtime() if result := bool(mtime <= walk_after): - self._printer.skip_timestamp("older than timestamp", path_info) + logger.debug(f"Skip: older than timestamp: {path_info.full_output_name()}") + self._reporter.stats.record_skipped_timestamp() + self._reporter.progress.mark_skipped_timestamp() return result diff --git a/picopt/walk/walk.py b/picopt/walk/walk.py index 149dc64c..0c418cce 100644 --- a/picopt/walk/walk.py +++ b/picopt/walk/walk.py @@ -1,32 +1,42 @@ """Walk the directory trees and files and call the optimizers.""" +from __future__ import annotations + import os import traceback from concurrent.futures import ProcessPoolExecutor from pathlib import Path +from typing import TYPE_CHECKING -from confuse.templates import AttrDict +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.exceptions import PicoptError -from picopt.path import PathInfo +from picopt.log import console +from picopt.log.progress import make_progress +from picopt.log.reporter import Reporter +from picopt.log.summary import Stats +from picopt.log.summary import render as render_summary +from picopt.path import PathInfo, is_path_ignored from picopt.plugins.base import ContainerHandler, Handler, ImageHandler -from picopt.printer import Printer -from picopt.report import ReportStats, Totals +from picopt.report import ReportStats from picopt.walk.handler_factory import HandlerFactory from picopt.walk.legacy_timestamps import OldTimestamps from picopt.walk.scheduler import ContainerNode, OptimizeLeafJob, Scheduler from picopt.walk.skip import WalkSkipper +if TYPE_CHECKING: + from confuse.templates import AttrDict + class Walk: """Methods for walking the tree and handling files.""" def _create_top_paths( self, - ) -> "tuple[Path, Path]|tuple[Path]": + ) -> tuple[Path, Path] | tuple[Path]: """Create and Validate that top paths exist.""" top_paths = [] paths: tuple[Path, ...] = tuple(sorted(frozenset(self._config.paths))) @@ -46,14 +56,20 @@ def __init__(self, config: AttrDict) -> None: """Initialize.""" self._config: AttrDict = config self._top_paths: tuple[Path, ...] = self._create_top_paths() - self._printer: Printer = Printer(config.verbose) - self._totals: Totals = Totals(config, self._printer) + self._stats: Stats = Stats( + timestamps_active=bool(config.timestamps or config.after), + dry_run_active=bool(config.dry_run), + ) + # Progress is built later (in walk()) once we know we're really running. + self._reporter: Reporter = Reporter( + stats=self._stats, verbose=int(config.verbose) + ) self._executor: ProcessPoolExecutor = ProcessPoolExecutor( max_workers=self._config.jobs or None ) self._timestamps: Grovestamps | None = None # reassigned at start of run - self._skipper: WalkSkipper = WalkSkipper(config, self._printer) - self._handler_factory: HandlerFactory = HandlerFactory(config, self._printer) + self._skipper: WalkSkipper = WalkSkipper(config, self._reporter) + self._handler_factory: HandlerFactory = HandlerFactory(config, self._reporter) def _init_timestamps(self) -> None: """Init timestamps.""" @@ -73,6 +89,17 @@ def _init_timestamps(self) -> None: for timestamps in self._timestamps.values(): OldTimestamps(self._config, timestamps).import_old_timestamps() self._skipper.set_timestamps(self._timestamps) + if tps := tuple(tp for tp in self._top_paths if tp in self._timestamps): + roots = ", ".join(sorted(str(p) for p in tps)) + logger.info(f"Loaded timestamps for: {roots}") + + def _dump_timestamps(self) -> None: + """Dump timestamps to disk, with a log line per top path.""" + if not self._timestamps: + return + if dumped := self._timestamps.dumpf(): + roots = ", ".join(str(p) for p in dumped) + logger.info(f"Dumped timestamps for: {roots}") def _enqueue_children( self, sched: Scheduler, node: ContainerNode, children: list[PathInfo] @@ -167,7 +194,9 @@ def _walk_file_get_handler( handler = self._create_handler(path_info) if not handler: - self._printer.skip("no handler", path_info) + logger.debug(f"Skip: no handler: {path_info.full_output_name()}") + self._stats.record_skipped() + self._reporter.progress.mark_skipped() return handler def walk_file(self, path_info: PathInfo, scheduler: Scheduler) -> None: @@ -193,33 +222,94 @@ def _walk_top_path(self, top_path: Path, scheduler: Scheduler) -> None: ) self.walk_file(path_info, scheduler) - def walk(self) -> Totals: + def _count(self, path: Path, name: str, *, 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. + """ + if ( + not self._config.recurse + or (not self._config.symlinks and is_symlink) + or name in WalkSkipper._TIMESTAMPS_FILENAMES # noqa: SLF001 + or not is_dir + or is_path_ignored(self._config, path, ignore_case=False) + ): + return 1 + try: + with os.scandir(path) as it: + entries = sorted(it, key=lambda e: e.name) + except OSError: + return 1 + total = 0 + for entry in entries: + try: + total += self._count( + Path(entry.path), + entry.name, + is_symlink=entry.is_symlink(), + is_dir=entry.is_dir(), + ) + except OSError: + total += 1 + return total + + def _count_path(self, path: Path) -> int: + """ + Mirror walk_file's recursion gate to count progress-bar advances. + + Each non-recursing visit produces one progress mark — top-level + files, ignored/symlink/timestamp-file dirs, and so on. Recursed + directories contribute their children's counts instead. In-archive + children don't emit marks (workers can't reach the live region), + so they're not counted. + """ + try: + return self._count( + path, + path.name, + is_symlink=path.is_symlink(), + is_dir=path.is_dir(), + ) + except OSError: + return 1 + + def _count_total(self) -> int: + """Total advance count for the progress bar across all top paths.""" + return sum(self._count_path(top) for top in self._top_paths) + + def walk(self) -> Stats: """Optimize all configured files.""" self._init_timestamps() 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) + # Replace the no-op progress that the skipper / factory captured at + # construction time so they advance the real bar. + self._reporter.progress = progress + scheduler = Scheduler( config=self._config, executor=self._executor, timestamps=self._timestamps, - totals=self._totals, - printer=self._printer, + reporter=self._reporter, max_workers=max_workers, create_repack_handler=HandlerFactory.create_repack_handler, child_enqueue_callback=self._enqueue_children, ) - for top_path in self._top_paths: - self._walk_top_path(top_path, scheduler) - - scheduler.run() + with progress: + for top_path in self._top_paths: + self._walk_top_path(top_path, scheduler) - self._executor.shutdown(wait=True) + scheduler.run() - self._printer.done() + self._executor.shutdown(wait=True) - if self._timestamps: - self._timestamps.dumpf() + self._dump_timestamps() - self._totals.report() - return self._totals + if self._config.verbose > 0: + render_summary(self._stats, console, dry_run=bool(self._config.dry_run)) + return self._stats diff --git a/pyproject.toml b/pyproject.toml index 6a4d1774..99bb77d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "defusedxml>=0.7,<1.0", "filetype~=1.2", "humanize~=4.12", + "loguru~=0.7", "mozjpeg-lossless-optimization~=1.3", "piexif~=1.1", "pikepdf>=10.5.1", @@ -23,8 +24,9 @@ dependencies = [ "pyoxipng~=9.1", "python-dateutil~=2.8", "rarfile~=4.0", + "rich~=15.0", "ruamel-yaml>=0.18,<1.0", - "treestamps~=3.0.0", + "treestamps~=4.0.1", "typing-extensions~=4.13", ] description = "A multi format lossless image optimizer that uses external tools" @@ -33,7 +35,7 @@ readme = "README.md" requires-python = ">=3.10" license = { text = "GPL-3.0-only" } name = "picopt" -version = "6.2.0" +version = "6.3.0" [[project.authors]] name = "AJ Slater" email = "aj@slater.net" @@ -64,7 +66,7 @@ lint = [ "complexipy~=5.1", "icecream~=2.1", "mbake~=1.4.5", - "pathspec~=1.0.4", + "pathspec~=1.1.0", "radon[toml]~=6.0", "ruff>=0.13,<1.0", "ty>=0.0.4,<1.0", diff --git a/tests/colors.py b/tests/colors.py deleted file mode 100755 index 7dabd778..00000000 --- a/tests/colors.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/env python -"""Test colors.""" - -from termcolor import cprint - -colors = ( - "black", - "red", - "green", - "yellow", - "blue", - "magenta", - "cyan", - "white", - "light_grey", - "dark_grey", - "light_red", - "light_green", - "light_yellow", - "light_blue", - "light_magenta", - "light_cyan", -) - -attrs = ("bold", "dark") - -for color in colors: - cprint(color, color) - for attr in attrs: - cprint(f"{color} {attr}", color, attrs=[attr]) - cprint(f"{color} {' '.join(reversed(attrs))}", color, attrs=attrs) diff --git a/uv.lock b/uv.lock index b907c06b..f6db4d2f 100644 --- a/uv.lock +++ b/uv.lock @@ -183,16 +183,15 @@ wheels = [ [[package]] name = "backrefs" -version = "6.2" +version = "7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, - { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, - { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, - { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, ] [[package]] @@ -292,11 +291,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] @@ -488,14 +487,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.2" +version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] [[package]] @@ -518,74 +517,74 @@ wheels = [ [[package]] name = "complexipy" -version = "5.3.0" +version = "5.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/52/6bd2b8ed58f8e7d7b37a9ed06da59332035ddb1846fb1f8f2f7a83cf4c2c/complexipy-5.3.0.tar.gz", hash = "sha256:b60b60c24b5f4e4eca1dd18f145f41f428c607c40477305b4f8302726b3e1eb6", size = 339855, upload-time = "2026-04-16T14:52:28.954Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b3/6def1cd68f238134da319cb4e04dc19f9ec588b240b1d73ba4d46e772a86/complexipy-5.3.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:986e3833b622047caaff3d48e5726251f80c46fdfb50d8f3fec9fc5afa6daba5", size = 2047844, upload-time = "2026-04-16T14:49:34.379Z" }, - { url = "https://files.pythonhosted.org/packages/3d/34/09c579607daceb8f0d9de5f94f336d5d0f640d48ec29b587d6a258be02ad/complexipy-5.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2cb0d86be1fc5e7cb8a3b64721c3270b1eff59228dc5adae4734f351a976854d", size = 1973121, upload-time = "2026-04-16T14:49:36.758Z" }, - { url = "https://files.pythonhosted.org/packages/6d/14/0cefa2aaf0c2dd6fdbf7961206dbf5e130d60b23bd62481a62a0f0aa6642/complexipy-5.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6fe1ce8804675b2b196091cf0779e0d1ca51058d71c1f15b4fb3a67cbcdf5bf6", size = 2141034, upload-time = "2026-04-16T14:49:38.6Z" }, - { url = "https://files.pythonhosted.org/packages/30/8d/dd5b4f5339fb8f4a92e76effca2e65ba7b72ffe2309781b5bfa5d971351a/complexipy-5.3.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c71058f43b6b48b50f1b835a97904bd15b0f82d4d3b1cc046a88fb166392eaa2", size = 2076627, upload-time = "2026-04-16T14:49:40.391Z" }, - { url = "https://files.pythonhosted.org/packages/ca/2a/1c01a21cf27d475c16a0fc444e66d53379da81fa3d73e2d7e8c1c8b0d6b7/complexipy-5.3.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ed84d9f8ebee3edd08a8d169594d19e36ff1e859eed4105d84d683010c463b0f", size = 2253808, upload-time = "2026-04-16T14:49:42.81Z" }, - { url = "https://files.pythonhosted.org/packages/1f/1b/267ee3908f04e0ef38b53a6eaa20f20088730f5bf0571e9d8bca30474df3/complexipy-5.3.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:21e653cbe981b764d5a39a15def56f554b869974a0e6a3e9d7150b66d84a3514", size = 2491650, upload-time = "2026-04-16T14:49:44.633Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b5/34f1b0dd013d3fe57a892b1849c13ca2b81f1aba7a1e514209a6f0163b48/complexipy-5.3.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6d300e92712cf43c69bf7f361f35ce749803f0025bc9d7d0801547debf8022cc", size = 2284213, upload-time = "2026-04-16T14:49:46.488Z" }, - { url = "https://files.pythonhosted.org/packages/61/20/1017a06663b16ea775d230d6ecbf9221cf0cb15926968e9ce0d3eae9f078/complexipy-5.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64331e481432b6cbea3856346d787453eae98586bc62efccfc30677e77b392a2", size = 2190946, upload-time = "2026-04-16T14:49:48.371Z" }, - { url = "https://files.pythonhosted.org/packages/b8/97/437aba4f416d25538945c38ff629471d4a6f7e6d1142575c9a2ae621729c/complexipy-5.3.0-cp310-cp310-win32.whl", hash = "sha256:e413bba9bf05ffd5f21d8cf0853e068d780f4bc45cb0fc3199d8a84782587d5d", size = 1772465, upload-time = "2026-04-16T14:49:50.122Z" }, - { url = "https://files.pythonhosted.org/packages/89/f8/6b37a3cecf2a0a0c4aec2bb67fd7a1453584edacf66aa995e56da61ac756/complexipy-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:98d3f3fffb472ca39ecdfbe06bb853f30ab1f800ed82d6047eeb18f2167c2fdc", size = 49498, upload-time = "2026-04-16T14:49:51.941Z" }, - { url = "https://files.pythonhosted.org/packages/b0/15/f7dae8fff8150bd0288e19204cb2a251c0a2b5ab3f63c4f6466e110964f6/complexipy-5.3.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a5684388a2ac21997e669fd6664cb48d39dcf3699b79a59209c3b0283473f39f", size = 2047961, upload-time = "2026-04-16T14:49:53.595Z" }, - { url = "https://files.pythonhosted.org/packages/a1/95/6e1dec9fda54dbb97cd3aa29c2287add4e502d2f7843fcadc7f2e5971e39/complexipy-5.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6bde4a3060818d267f1b12bff41fc8dd0f28175730e8899669373831a016353", size = 1973213, upload-time = "2026-04-16T14:49:55.241Z" }, - { url = "https://files.pythonhosted.org/packages/60/19/c92daa4a11f3d19cbbb8d68ff12197a4996f1125be05ff77141f2e19b361/complexipy-5.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7dffd3c512ddba2259109a7ca8c79843dadd986755c8498b35a3e242eeeac0e8", size = 2141075, upload-time = "2026-04-16T14:49:57.231Z" }, - { url = "https://files.pythonhosted.org/packages/94/63/d0fb47745cb8e243aa536e43d4beb4d49ea0d3c5a3b7f577c3568bce2431/complexipy-5.3.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:7521d85dcfef17ca797b89508ba4db43d4f017d2389fa57bc144e5882e1b2c8a", size = 2076682, upload-time = "2026-04-16T14:49:58.996Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a6/6ce3cd103e8ef1084504897904b48e6edee8daa64b8c8836174ce490741f/complexipy-5.3.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5ae64b84f0e9b83844c10a6fe5a0610348176560ed5a89e2b33ef0858f48b720", size = 2253949, upload-time = "2026-04-16T14:50:01.08Z" }, - { url = "https://files.pythonhosted.org/packages/57/6b/e09df53b28fd82b85cef6ad98e03e150b2148338ddf5ecebefbddafe8b11/complexipy-5.3.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f800807a050cbc37a12071bc8d86356a2c06a88285bf52e4be9f5116f4ab444", size = 2491763, upload-time = "2026-04-16T14:50:03.514Z" }, - { url = "https://files.pythonhosted.org/packages/03/05/5a4f010132259c85c2ddca3ad27da8ebb7acd6a291fdd696d1883c12b43c/complexipy-5.3.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:403902d32189102cfda5462884a302d847234b3267d14ab5ccc2f77160c37710", size = 2284052, upload-time = "2026-04-16T14:50:05.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a8/46ee1f5bad3ce838da7d5495693582d8cf29d85ae188f7e2716daaa52967/complexipy-5.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:033cc22618d958c123d62c377c953aef466a3e8774b595c6c8604678c6c3c114", size = 2190673, upload-time = "2026-04-16T14:50:07.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/13/d17c1416fd530767d80fcee27f78265d2777d710ac8ba1fa50eb572a4053/complexipy-5.3.0-cp311-cp311-win32.whl", hash = "sha256:42931056b1f92364f859e2798c93e49c66976899af144fce74b2aebb8fa84079", size = 1772460, upload-time = "2026-04-16T14:50:09.667Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/059b802ea28c4738f547667a08b6265ac0d5bab4bc571142f30ec18d850b/complexipy-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:a7b2f0cdeb5fc3a395c7215d1d82f28e498bb154d8d95dbae05895fbf0d6fbde", size = 1886367, upload-time = "2026-04-16T14:50:11.787Z" }, - { url = "https://files.pythonhosted.org/packages/df/04/d155ac86edb86f35fcde93231e3c4bcc9676800e67d10a087f3e80200de5/complexipy-5.3.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:04d6137eff0a17e6fd352a6cdc7c7fb6fdf4200a9e6db3f627c60fe79b82cfe3", size = 2047114, upload-time = "2026-04-16T14:50:13.556Z" }, - { url = "https://files.pythonhosted.org/packages/cf/02/32f7417d5b0b4ddfdf5ab03424c1f193e7633ba1cc4262f768574e628fe2/complexipy-5.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86680349d5f1f72b1e512bc045d3f7999941c8e7f5dfe235c308a0556265888c", size = 1974763, upload-time = "2026-04-16T14:50:15.263Z" }, - { url = "https://files.pythonhosted.org/packages/be/8b/533b8b566ceef7ab1d8958cb653207454319e4de2fd4ebae5cb38d861b57/complexipy-5.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52f6fad6e998a3d6a03c737ed0a915565ce8f90c51921597e7156a717b65110d", size = 2139705, upload-time = "2026-04-16T14:50:17.163Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dc/3421c3e6ba82bb8eeab5d3ba29c881be5c74e0ea6e31d0a30e5c870c6079/complexipy-5.3.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:a5bdd1ded9c83e0cc0f2d47c21e3066aa7443cce2d9b556d0915cb407ac26af3", size = 2074917, upload-time = "2026-04-16T14:50:18.845Z" }, - { url = "https://files.pythonhosted.org/packages/c4/9b/7745ce6f0df30bb9baa4fe25f8f3cedd2222355d6f4269c247fbffff7210/complexipy-5.3.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:474270be61ed0666e26b8b6f2c40b3b131ef3baf647a290641e894c9f24062aa", size = 2253827, upload-time = "2026-04-16T14:50:20.743Z" }, - { url = "https://files.pythonhosted.org/packages/e0/07/e9bf57fbdf37bdf4a526d1938ef461e088233dcc7e43088032e47cd2ab24/complexipy-5.3.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a93322f7350f8ac514384986794309e2f13d4593b6ac579c31d3ef257190e3f0", size = 2490425, upload-time = "2026-04-16T14:50:23.064Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d4/3cdfbbb951d6c5e17cd62b6402fa1698572f155b40a205b70c9573ab54c7/complexipy-5.3.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c0834678ed93729d72bfd9be72e19abf38cce5094ef18fb66a7ed64c6eb05be2", size = 2280642, upload-time = "2026-04-16T14:50:24.812Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/e04d46e5b4a4d46f112db3296137cc3593fd1819a98540122ce2b6bfbd19/complexipy-5.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80cc9cefca8c8cecc75096f2a9b207f050d009f2dee5c32d909c1db7a60db747", size = 2190510, upload-time = "2026-04-16T14:50:26.656Z" }, - { url = "https://files.pythonhosted.org/packages/f4/e9/3008af7872cf29fbebc842c41d6e7e428dca154e49e9e99a38a78a420159/complexipy-5.3.0-cp312-cp312-win32.whl", hash = "sha256:8b1a0f88cfc0214e4c7dbd328c92d79046e4be64f8f58eaecfd3fc59d567de12", size = 1774088, upload-time = "2026-04-16T14:50:28.632Z" }, - { url = "https://files.pythonhosted.org/packages/be/ed/83a87dd3e8129d60eb2fc9e6c87fe920cd2cbec410a442998c3fd66e746c/complexipy-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:6415b99c675115f4ef24136c9b5baed06c61e0b6416620d012baa7cad33419f0", size = 1886883, upload-time = "2026-04-16T14:50:30.439Z" }, - { url = "https://files.pythonhosted.org/packages/2f/19/7c702da29f9261f51c8c5a3e50591cf57cc0700be8cea75cd09eddb5e7fe/complexipy-5.3.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea9b82c62b872079ad61f643b1ef2459e4008285d95b1671b2e36167f5c255f7", size = 2047118, upload-time = "2026-04-16T14:50:32.236Z" }, - { url = "https://files.pythonhosted.org/packages/90/cf/2028c19f62b69bbd4805e1cf5b219afa965bb4fb1eeda5f8e169f76e03ac/complexipy-5.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb55e0e5a15d63869af7bbbe51cc92e2432233d571b1d1089bb15e3b50f0bb62", size = 1974763, upload-time = "2026-04-16T14:50:33.982Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f2/bbdc38bbce6ec6723c2222a554a9d8a79e64ab4c9d512ae1d4bb1fa2c865/complexipy-5.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dc3099aebe6f885cfbc73229c3f137f5491abe355c74a300616cb6537c2a14dc", size = 2139705, upload-time = "2026-04-16T14:50:36.183Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b9/548d920242b68a32fc25622cea7118765c0edb8d87dd46a3dc0353b8b9d9/complexipy-5.3.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:b2a4c31158f1f31d3c059384878adfd2edcebe2037de36125e14101a3316d110", size = 2074920, upload-time = "2026-04-16T14:50:38.351Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9d/2dd3f2d017e4dd5738da39ef81df2b9b4330d64bf0a711bf5d818a0ba065/complexipy-5.3.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5a679dd88e03c36a003190e912d0cf953c767cf01dc6669b05f4941aad5fdab6", size = 2238195, upload-time = "2026-04-16T14:50:40.352Z" }, - { url = "https://files.pythonhosted.org/packages/25/39/71e94224129f713d8379e100ac35970effd1cc3c5d8beb48e56359c97c85/complexipy-5.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:13c7cb4ce9946504539d77c718772331056b5ab11e9b1a3e791e79f43dee9208", size = 2490427, upload-time = "2026-04-16T14:50:42.741Z" }, - { url = "https://files.pythonhosted.org/packages/2b/4e/d61e5d42606b01c31c3718975170e28dac39a0c7d340758f9fa87ace70ca/complexipy-5.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:297fa947410db3dde3e5770d06148509b560992b251a1bbce7035505dcafe8b8", size = 2280643, upload-time = "2026-04-16T14:50:44.966Z" }, - { url = "https://files.pythonhosted.org/packages/b9/49/e5db595955545a111597d268ba249391c298c5e3c9751e4d96c6ce6b4983/complexipy-5.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c5a1966bd608515064e6e5ef63713bafe222561858549c0cb286518b99ded510", size = 2190510, upload-time = "2026-04-16T14:50:46.817Z" }, - { url = "https://files.pythonhosted.org/packages/b6/7f/dce9872871703c08a75d708179f16be3b66021cc3056f3878161a90e0ea7/complexipy-5.3.0-cp313-cp313-win32.whl", hash = "sha256:347f1570feaff948085dcb6eb2af4b97ff6c724cc2ee70e7a91a340ae2941c0f", size = 1774080, upload-time = "2026-04-16T14:50:48.571Z" }, - { url = "https://files.pythonhosted.org/packages/92/ae/40fe96054305aada53bd2093d041d7298b01a33b2903d905bf39568e78fe/complexipy-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:34789f2c9b64ccc2c03efb61ad6c96f3b3a53cbb472caf8c389bbca3067db4c3", size = 1886888, upload-time = "2026-04-16T14:50:50.366Z" }, - { url = "https://files.pythonhosted.org/packages/3f/09/fae9f39d7aa6790991c9b037ca09640e8d6954031f86834db093c1c70131/complexipy-5.3.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:76c57b535e09546898aa560347f489e596b721c26bd748fe12a882f22c8c5804", size = 2047116, upload-time = "2026-04-16T14:50:52.14Z" }, - { url = "https://files.pythonhosted.org/packages/ae/fb/162030a9f8991341c847a1993677089509c19d71cb159e332703bda1aba8/complexipy-5.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0bcdbe80efaa6d9ef5d25358bf78862fc905e1e9b1bcab2763b208c1e1b135c9", size = 1974764, upload-time = "2026-04-16T14:50:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/f4/46/9f80dc4dd153436adb2715c7692699c9f48e5e5f96c2e70e15ec16954740/complexipy-5.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:833bf17a31eddd5c694b84a571f04a9c07c226474cbeefd23e832d34e93877dd", size = 2139705, upload-time = "2026-04-16T14:50:56.135Z" }, - { url = "https://files.pythonhosted.org/packages/28/25/78f2bb05ef2dec40ad76c07c81c66b50486e44a7634be034ccd905d00ebd/complexipy-5.3.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:3b5076647fdd18f26a832e831e85946f65cb665832c0498ea4a0be1b9b0659ac", size = 2074917, upload-time = "2026-04-16T14:50:57.977Z" }, - { url = "https://files.pythonhosted.org/packages/1c/d5/3ef542f345c1f4e72e5f47eb2bafe286351393a009a13deae33889a1cd33/complexipy-5.3.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:508d4eabf52061b3877e0ec67937db34f7414b20ddb67dcf1a43aba3d090f5cc", size = 2253826, upload-time = "2026-04-16T14:50:59.79Z" }, - { url = "https://files.pythonhosted.org/packages/15/fa/2feb0beb5c96d9df5caa28597d9825f07a975760cfb78b7d6a55bb5ccbc4/complexipy-5.3.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:07654b531e49ae6788e276c5b7f26c5cbd8e785b788b4ec2af00c48f38ba46b5", size = 2490426, upload-time = "2026-04-16T14:51:01.674Z" }, - { url = "https://files.pythonhosted.org/packages/3b/29/f2d61cc70901d0b8b307e1245977682a4d82b7eaad832b33b4f867942ce8/complexipy-5.3.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6d30def78c5ccf64edb006e1185afd07e9504ed04aee850941acd86a73bc7fe", size = 2280643, upload-time = "2026-04-16T14:51:03.99Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9b/ec85f23523f5f33f40f5bb1895780a6f81bcd27231cec70ff026cd375331/complexipy-5.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e651e094e09c0b3b66bb320a4091177febd59cb7326c06a442c161e2d90c0f9c", size = 2190510, upload-time = "2026-04-16T14:51:06.084Z" }, - { url = "https://files.pythonhosted.org/packages/4b/af/b4109e1f44224995107de4187a9dcc203417673b0d0af2e4e45dfed19466/complexipy-5.3.0-cp314-cp314-win32.whl", hash = "sha256:f1596e5e535c278ea3308f98e033ec99110f80b44db3251f4992d6e5f9f5c402", size = 1774081, upload-time = "2026-04-16T14:51:08.123Z" }, - { url = "https://files.pythonhosted.org/packages/7a/17/64927bd2d28c16b736b540189fe10de46d8afba9ed6d8b32636776901d45/complexipy-5.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:f5daedb399a678ac176e31cc0b7a2b399a5382c3fe4924ff89d3c052af6a25a2", size = 1886889, upload-time = "2026-04-16T14:51:10.321Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8f/7ff499305d090285471597c61256cf2f2b11854f435042f807b2d4124695/complexipy-5.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08d4c8f45173bbbeff81d36ed9f637bb8084bc9df2eff58e81f7a6d99bd8ffe8", size = 2140856, upload-time = "2026-04-16T14:51:54.662Z" }, - { url = "https://files.pythonhosted.org/packages/88/84/fc51171c5cdb48296dfa3ef5e64801146c6ec09988683672347565c268ad/complexipy-5.3.0-pp310-pypy310_pp73-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4d318b2ae1ae0ab69356ee42c3c5010d6e143aa7602e8e5719d41ebce8fac09e", size = 2075396, upload-time = "2026-04-16T14:51:56.386Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d4/50aad93204fa702811554ae86ae5e820518c31e49142f96801f496a81af0/complexipy-5.3.0-pp310-pypy310_pp73-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3456c3c86b677d3c460e11b9bd23526454eefc2c53b0c3650aa2fc7ba0dd2262", size = 2491888, upload-time = "2026-04-16T14:51:58.144Z" }, - { url = "https://files.pythonhosted.org/packages/e3/79/1c02993e7530b53b4b7dad4e0fd8b8a20d10d5656fef5a062f878b02b3dc/complexipy-5.3.0-pp310-pypy310_pp73-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ba77e7ef31ee2c44dac896e1ce4180d046839f2bc22da653c556e82726564537", size = 2283719, upload-time = "2026-04-16T14:52:00.071Z" }, - { url = "https://files.pythonhosted.org/packages/c0/85/e59dd22b3b8cb5d98196c85bd571adaa587c5752c6987023cc68791dcb31/complexipy-5.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d03454a9bd650ed56dc694a8cb985b020ff792ca5be6208f5bc7c9861bfe3521", size = 2140907, upload-time = "2026-04-16T14:52:01.859Z" }, - { url = "https://files.pythonhosted.org/packages/35/2f/c8389df2b7862ac516722f81ee95c5d4f1dc63e4f4ef7d87a917b87bf3d9/complexipy-5.3.0-pp311-pypy311_pp73-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:91e9bedd18d693cc4e5482e76a52a53298e831020eb6fdb4084b89d5547df78f", size = 2075471, upload-time = "2026-04-16T14:52:03.696Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bb/83f64867544eec1cb3679d1400e5aea44dc34db6463f1a90055e8cc59de9/complexipy-5.3.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15b7c1f60c4ec73f4fa180a5e1ebd2b2db1aa16b2de2612cf2313b97396552c9", size = 2254013, upload-time = "2026-04-16T14:52:05.589Z" }, - { url = "https://files.pythonhosted.org/packages/f4/8f/43cc653c6cc7bd5a58a3dfc86c64b3117b448c2ca5d8446537836626c547/complexipy-5.3.0-pp311-pypy311_pp73-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6ee0dc76a30a282ee1869723abdefbca9c8c1c94b3088ed50ab2b109d7019b4e", size = 2492045, upload-time = "2026-04-16T14:52:07.542Z" }, - { url = "https://files.pythonhosted.org/packages/91/66/14bf24cfb71f870ad5b27504960377fd0209f56fe8571a6dc7416317ccbc/complexipy-5.3.0-pp311-pypy311_pp73-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:46622e5bd80e0aaa1ca9bd333f7cc71d03007d71fb7016ea2a68b404b9c8e3d7", size = 2283712, upload-time = "2026-04-16T14:52:09.587Z" }, - { url = "https://files.pythonhosted.org/packages/d1/90/6c3a47b627170885cca0c3edef418eb8680ba8fa6efb9435a16c4b416d05/complexipy-5.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7e24f151789ee320fff80cca1ccbde9ff3905ef4a88a3dbcbc83d29545c4c232", size = 2189627, upload-time = "2026-04-16T14:52:11.746Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/7f/42/830820022dad5b7444a485ddd900f1a076f35658ff384428b7be8f20ff9f/complexipy-5.4.0.tar.gz", hash = "sha256:eecfdf77821839c79c3853d0567235a7773e8037ed896a897487350251f68404", size = 339507, upload-time = "2026-04-25T02:11:19.771Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/8a/68a52ac3e90e143542888cc31b93bceb29af335bbe9e77c3c4f63c45ebfc/complexipy-5.4.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8e6e886ef933cc3ec2cef0f893282e31c33504792b4aee2441897e4a8b9c37f3", size = 2033085, upload-time = "2026-04-25T02:08:30.156Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c8/a8c2adb3eea4023e9751c69b393034bac0dc773936ddd1adc3ac84d9f7e7/complexipy-5.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc6fcd0dd983398f611bde1d108a998ce77238db2b977669d5d90557957bb582", size = 1961969, upload-time = "2026-04-25T02:08:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/120e8e64395e4cca59e8505f73a5d4599ec750ef8d732e6bc00c93c3124d/complexipy-5.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0253fae6f08a56e2d35564b0070d164fa5dde18c6e8927fb2fcda87e9448293", size = 2138629, upload-time = "2026-04-25T02:08:34.386Z" }, + { url = "https://files.pythonhosted.org/packages/3e/16/456cef4a8e24889ca6f408b0f729f847f9399a430ec1b810e0e59f21b79d/complexipy-5.4.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:88b53ce470315438bced6338960ca28e6b1c0e7d1bc37f9efc9370c6dc4c0ff4", size = 2073362, upload-time = "2026-04-25T02:08:36.352Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a2/13437e2521eeacd8a95ab72a9521c3ffef824ff93abc6f464b0de9b2c791/complexipy-5.4.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a4d34148bb62f2ef9cf196c405bda2046ad2c84ec52808879547f1eab2211452", size = 2250753, upload-time = "2026-04-25T02:08:38.494Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b8/c6fd07596b5a413590827bf81001a526c765f2abe379f62646623606990d/complexipy-5.4.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:cb485cd8b708bd4df837189282926c46c280f2ddd8470e9bbd0c8ffd7055a47a", size = 2488482, upload-time = "2026-04-25T02:08:40.786Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bb/905ade853d92b2cacbdc7ce7b1dad82ab9844024db025bf582b8b1ef2a9e/complexipy-5.4.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:e9c1850f475c6cd55ef17223dc26682ed37a9d4f5f7566d21598f0ce00b36ca1", size = 2283216, upload-time = "2026-04-25T02:08:42.747Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/138856219acae12a84815a6ce0e00e4f11ae3167d86f3d3bacf2fb386ff3/complexipy-5.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7684fdb587b42e72e14fbe97c91b0e415ace0a3ee16a0451edd87ad524de5621", size = 2188268, upload-time = "2026-04-25T02:08:44.405Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/7e7877fca35607251ebd574a5bbf3d1913b849d45ff8fd75680df9163b0c/complexipy-5.4.0-cp310-cp310-win32.whl", hash = "sha256:a091ee0e37ecf263101cf4c0cd3ff77e80a73da01eff314d8f14ac1ed0f2365a", size = 1772457, upload-time = "2026-04-25T02:08:46.097Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/e9149b1f956499234bb23078b6d5317d9cbcd233d94e0abaa86bd9563cbb/complexipy-5.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:4cdfdb00bd8e887fe2948a351b46d6a55a22645c1d3b5462089596a6427ae537", size = 1884147, upload-time = "2026-04-25T02:08:47.826Z" }, + { url = "https://files.pythonhosted.org/packages/df/cd/eabb9d2e4b625c28feb962f07fe6192c16daa089a29abec8661086129ad0/complexipy-5.4.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3fba640f0920fc130dcca0414e269624c15d676c2bb4a4e53352befcf01d5f43", size = 2033123, upload-time = "2026-04-25T02:08:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/d0fcb0ca7564eaf61b825a97cd3b798d3995f66066778c143b66438cbc77/complexipy-5.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ca4f5c5d0c89e40bb4c19f0190a9ab4a1d5fe20b2c6fb10659795ccb19f967f", size = 1962073, upload-time = "2026-04-25T02:08:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/72/f2/52f80207e5c94c4224328f4e51e55fa943221698a418e10a6c7bae1ed08f/complexipy-5.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9f7598dffe65271daa8f0ef82433ebd4224104650962dedbf5ed27ac1f1ea76f", size = 2138599, upload-time = "2026-04-25T02:08:53.804Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/31a8fc7b2c7028b8c6e59741dd1e8db6ad0c585ac4690de8f5a306dea132/complexipy-5.4.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:0ab5c9e6cd1c5562cda89137ddc6d4960c5eac93310918658d35a592ff1fe3a9", size = 2073430, upload-time = "2026-04-25T02:08:55.506Z" }, + { url = "https://files.pythonhosted.org/packages/dd/43/d9c4a2621a1088f6e07c6c4d9d3e512b96aa86a0aa42fadec44250beda7e/complexipy-5.4.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:860ed17eba40cda83b3685c8121844cb06e3cbbbcd90fe59f3d0200ada6ca636", size = 2250948, upload-time = "2026-04-25T02:08:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/13/40/5d774d72ad398d59db440c99f9d9262a6a0b33be3cbf975625265a1729df/complexipy-5.4.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:07c71369e3eed0a675fede71ab0da91f1d3441a8070e51de28a9c2125070b872", size = 2488565, upload-time = "2026-04-25T02:08:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a8/cd9d984034c5b2dd4583eaf88e51482aeaf4ef5b347b87b4c3234ae70ef3/complexipy-5.4.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:bf9fff738f10b02fcbc5dea463e7f614384b8c1a8bb09775fb05a29e3f643ed5", size = 2283076, upload-time = "2026-04-25T02:09:01.011Z" }, + { url = "https://files.pythonhosted.org/packages/7a/eb/90d3cfd961392d9f37cdd737adb2712beab37561fcda009aea23bfa1e61e/complexipy-5.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aeef65a8ce9cadc7343adeb3991026b2ac14fafef3f9c73ee25078a2b2607a1", size = 2172991, upload-time = "2026-04-25T02:09:03.186Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/c86f75b0bc1280f48fef54b95099636b494e970112780179d44558b62829/complexipy-5.4.0-cp311-cp311-win32.whl", hash = "sha256:a476d0d00e5012c2713915d854474fc564bd142f97a464d235a6cff71b5b3501", size = 1772457, upload-time = "2026-04-25T02:09:05.187Z" }, + { url = "https://files.pythonhosted.org/packages/cd/51/bf583b44d9c595b863bd240e482de29281ab7bb1ef361694fd591af9756a/complexipy-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:a22e6e20823ae0208451e6ed60e9230a0161c331db4d09c6c94ce4eb0a0b76fc", size = 1884148, upload-time = "2026-04-25T02:09:07.244Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/be547f8328f0b9ca722db22763df7ac7dfe0489c40cc9b26c552e4d47918/complexipy-5.4.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e5ff5f785cf903dbf59121cd489c413901d79f19ccd43e4ca590501b14accdab", size = 2032846, upload-time = "2026-04-25T02:09:08.893Z" }, + { url = "https://files.pythonhosted.org/packages/dd/64/2e47784f986a7bdd929bc3416a43982396156b9d59ef43d3a2a40bd22e89/complexipy-5.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7bb23bd23bdebfc7193821dcfb53952eb46c07404c0d94d04944d69abb416c81", size = 1962200, upload-time = "2026-04-25T02:09:10.657Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9d/1e2cd605b3549b3f80f99f2d42015769f040f6d1dd1fcf7d68e47cddb5fc/complexipy-5.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee36d113becdbe44ee45183c0f8042aea6c81d1f9e78f843012afe2c962a2bf6", size = 2137850, upload-time = "2026-04-25T02:09:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/4f0ff203d12d6da8c5fac9d9410bf24b198b46a7f83b3b5cf67d8610e58a/complexipy-5.4.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:72f3fb62cde1feea36b7cf01bbbc7f83ac0de27e682152bdab2d7fa89a8cdb01", size = 2073192, upload-time = "2026-04-25T02:09:14.22Z" }, + { url = "https://files.pythonhosted.org/packages/c8/eb/5d514a7d559fbc078fb9f027a46da47986d4f6d7e89c3ea57c3e207b4122/complexipy-5.4.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b9cbe2477fbe259ad9bcf3ab4b1e4489afd10145aaf43c2ad1fd31ba9d5dc667", size = 2249686, upload-time = "2026-04-25T02:09:16.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/f48bb0dd1c24ef72874bf3e217cb770eab3919138b599efda92e1c0706e2/complexipy-5.4.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:224f166699fd4d2044fb06ba0ac2de0c9961f76e5889ef4dc7b42863a302a3e6", size = 2487573, upload-time = "2026-04-25T02:09:17.903Z" }, + { url = "https://files.pythonhosted.org/packages/29/2f/a0c967b8856a8fed630f4fd48d4f872c16e73dabfe28057c1e4138ad071e/complexipy-5.4.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:00395ca31d033695d603e89f3707087badfa44b0b2316a58a3fca94fdedf3617", size = 2279451, upload-time = "2026-04-25T02:09:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/3e/50/25ac85c94f8968db0bbe0e105e407e3885e1aea93b90eef03bfca77131a9/complexipy-5.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:48402ae78a0d328c29262bd5337833ae633be5e1679e3019ee508bab621bfc9f", size = 2172664, upload-time = "2026-04-25T02:09:21.849Z" }, + { url = "https://files.pythonhosted.org/packages/ca/32/7a3a3c7b58e166f815ab65f4a03aba8cf25e387593d303d830c64f292f06/complexipy-5.4.0-cp312-cp312-win32.whl", hash = "sha256:f8c9a1256534b180d4e7627fc534ff8a20a6e75989ea041f878b61bb1318f083", size = 1772613, upload-time = "2026-04-25T02:09:23.482Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f4/382a824d750b84f26bc55edc9713c6c7c631bd5292dc4631d40ffe8ce250/complexipy-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3fb6757cef095d400dd374645d6c97c4041c0148a7c9042dd3d4ad50c0021d62", size = 1885135, upload-time = "2026-04-25T02:09:25.215Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f5/d865e45926fbeeac82790d140cf0466b994b4f09efd2340ea6107c7535c8/complexipy-5.4.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1d4e57e38d7f81b0859bbfea7b0811bb0278fac76ecbe5349db519ddcab1bcd2", size = 2032847, upload-time = "2026-04-25T02:09:27.213Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0d/20afb84148749774255eedf956374e3cff08c1037e5626d8eaf5e0ccccbb/complexipy-5.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b6506a346f76680e02707d7f8b15736262c80f75efc0f58b0b97475f130d389f", size = 1962201, upload-time = "2026-04-25T02:09:29.208Z" }, + { url = "https://files.pythonhosted.org/packages/65/14/735bb1c7c675ee9335d73812af3c79b2eee316d4fa8907a9fc65281988db/complexipy-5.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:70c7da3dc24155059239837f13c2d22ec1cd7bb29a482b8db458e298e5c92e38", size = 2137850, upload-time = "2026-04-25T02:09:32.025Z" }, + { url = "https://files.pythonhosted.org/packages/48/9a/0da7a8ebcf3de5029e6b2be6c308f04167b887d5e8e58b33bf7ee5e7f959/complexipy-5.4.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2776e41408dd6103edfe2427f80e01b3c5b5f23b37b203d1d6571242f275af7d", size = 2073194, upload-time = "2026-04-25T02:09:33.609Z" }, + { url = "https://files.pythonhosted.org/packages/79/20/fc04c1c48bd1dc30fdccbcd0984adce0555e33b5e19aacc9fe4c7df0cfb1/complexipy-5.4.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6ba5e652741801096f0b5b5a365d05f4268db1b6f26cdc91ad54844d03de980a", size = 2249685, upload-time = "2026-04-25T02:09:35.273Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/3e7856b38c61d0ba694a19f29dc6e871ef6f7924eb54059bec010cccd38e/complexipy-5.4.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bb7586443257586cc1ab1c524bb71d8553e7acee433d78b4c1b2f61825237272", size = 2487574, upload-time = "2026-04-25T02:09:37.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/77/36b2cd71d1ea29cef14c91755095a14a77d308a8d63684e286a9372efb66/complexipy-5.4.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:96f92fea96b97726cbbcb2b7f5979ab4c8d3fcb351a4c08f9ec0d58e1aba0ef9", size = 2279451, upload-time = "2026-04-25T02:09:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c9/ea4364c82e4f97d267c873a1aaa06c5c187a83e98f55ebd92fb3c5c174e6/complexipy-5.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:037d9b14f9297e37f8a5752b3f0c26bb5ed70dd62b7b033f92ffe63e9dde423b", size = 2172663, upload-time = "2026-04-25T02:09:40.504Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a3/fa6a852c62f69807115d0055178df461deaa5fd889e0a20dc013619e1737/complexipy-5.4.0-cp313-cp313-win32.whl", hash = "sha256:48b0ae7706a6c54da8ebfe0453d3a4b962e1300cca5db28c1aa3575fd1a61df2", size = 1772612, upload-time = "2026-04-25T02:09:42.497Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/e8cec169a787555920c19a58282676ef564d4fb40cf03fe1766f1ecb8a49/complexipy-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:8a97b47ff486a3607c1331540e8ea2b401babef1f95ec3dac079e17cc5e65505", size = 1885137, upload-time = "2026-04-25T02:09:44.672Z" }, + { url = "https://files.pythonhosted.org/packages/33/91/cf15faaa19e1dddbc2e940a07ade3137f8dc6fb428b75de2cf53d2b54b4f/complexipy-5.4.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d11519de249f0dae9e14eb12bfb32a41ce18720ca6506fdf23f8bbce3a300030", size = 2032844, upload-time = "2026-04-25T02:09:46.623Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9b/b39ebd45c1d097a43b2630ce5e634cb54327ec3d133cb590cb61f5e31839/complexipy-5.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ef21d5fa4bfcd9851a300ff08f3eea85ddd21afda656ab74198d01c74913d33", size = 1962200, upload-time = "2026-04-25T02:09:48.254Z" }, + { url = "https://files.pythonhosted.org/packages/f8/da/2aa7cb0a598ec162831f9c16ea6c0b09be27960defcbb574ddf203364442/complexipy-5.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:488097d185265556e64fec3a31d334f684ec55304bbb2533bd5708b4add420dd", size = 2137849, upload-time = "2026-04-25T02:09:50.481Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bf/3464fe2401594f5e435c282dbce3599a3eee2de38e413f763e7639e35b14/complexipy-5.4.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:43dca0070306007915b3c022239031df652a2c64ae0d3ffa10ba034d40b96226", size = 2073191, upload-time = "2026-04-25T02:09:52.236Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/c233e04ffdde42daa919b4c1470c47dd07450bd263dc66b63be1d68a2c88/complexipy-5.4.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1d8fb02e164264234e0d664d6d71d85a93706a77fdc54e603bc609a7fc9037d6", size = 2249686, upload-time = "2026-04-25T02:09:54.231Z" }, + { url = "https://files.pythonhosted.org/packages/ef/69/75103dd6a5bfe7e9fea8612f9641bb9ac1d23d748e6a353c6135b3b9924f/complexipy-5.4.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:101938466d910088d81f5494637ed8da992c93f417a69defc1e07b09771c4c1d", size = 2487575, upload-time = "2026-04-25T02:09:55.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b9/90f155eefa3aa10a6e70a94a8c7e0b06f19e37799c0c842b22b01e6a2824/complexipy-5.4.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:bbaed28e0bbfe527df43534bf0ba67b1459c52b55a46c01b802f227427043760", size = 2279453, upload-time = "2026-04-25T02:09:57.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/77/0e645595d374f444f4838a44e3b125a58b0691fdd46d6702a805665f9e05/complexipy-5.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5dc5d1db0c7d2befbf02e45a5ed2a64276c1ba4b710c1e86d031244eb33dea1c", size = 2172663, upload-time = "2026-04-25T02:09:59.723Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a0/ea630fe40f278b3f83230d58cff18e89f3f7ac3c526755067c4848990998/complexipy-5.4.0-cp314-cp314-win32.whl", hash = "sha256:60cbcdd503e869fdf314db4594cd0220a0b5d6f0432c4fc4ed49f92e635917ff", size = 1772612, upload-time = "2026-04-25T02:10:01.43Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fd/dd2504fcbbd04e1e7c4866e0a831ad8ca9f2513883049e91d8a4014b6b3e/complexipy-5.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:d2ef116c563e98b16c7494f6c8f1b2ddf47fcfc8f6665f7db98fc2b8b24087d2", size = 1885133, upload-time = "2026-04-25T02:10:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/54/40/2d7806f2f695327fc490c10d1cb48ba93ee44e8c118f7fdfee784a557706/complexipy-5.4.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:476f7d56eac53c056811d581e6583a69bd448f9ae6575f8c74c3f5cc2cc31fae", size = 2123539, upload-time = "2026-04-25T02:10:44.598Z" }, + { url = "https://files.pythonhosted.org/packages/02/e9/fe06c50947a8cf045bb83d8578fd1e7ad9be6dca22e6f085b496db561126/complexipy-5.4.0-pp310-pypy310_pp73-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:8a0679ecfa5bc30e18e297b33ecf77719e0dd3f3594d986402f849997f24a2c8", size = 2073833, upload-time = "2026-04-25T02:10:46.674Z" }, + { url = "https://files.pythonhosted.org/packages/47/e0/388951a4b5599d645dd0674ba40a7e74d0245c013b7a916c5dac68b261b1/complexipy-5.4.0-pp310-pypy310_pp73-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:84224a066b73c782bd1fb030a6f04978a16c4eb62cca96092d07f3da4bef7699", size = 2489551, upload-time = "2026-04-25T02:10:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ff/ac9c69942a876c056f98475b0493bdd1a7dd54f5ef928b815d8b0005602f/complexipy-5.4.0-pp310-pypy310_pp73-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5d0abc858e6761e5cf452432e029700516b8ce6e0521b7f60d30cdbad446489f", size = 2283188, upload-time = "2026-04-25T02:10:50.538Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/b1414675ff32c7ab8f4d450ed20f3eb923bd436837b447bf32ba9e11b078/complexipy-5.4.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0052d64c42d56e64173f4c808c361e754a61ec09de6435d5e469bb0eaf191d4", size = 2123576, upload-time = "2026-04-25T02:10:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/b4/80/136aef2720fe389102512975064d62efa1cf32864e955b697bd965ab0adb/complexipy-5.4.0-pp311-pypy311_pp73-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9a94600c379712c72602180cc4694e2c8e5791e8538080d6bfffea45dc68d922", size = 2073884, upload-time = "2026-04-25T02:10:54.501Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c7/bbb508dabc334802c3106bc1538075939bffbd9e385e33102da5fd250ea1/complexipy-5.4.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a76a94be193ad427f60ac985678b9bea8c34a34b023b0f8cbad37eec05975e22", size = 2250362, upload-time = "2026-04-25T02:10:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/4fca50804bdb0872a515d8f1840f05f57a705c53b34668deeb9af449f540/complexipy-5.4.0-pp311-pypy311_pp73-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5ce1df8f659263ab23610af5d3e9ac9c18b665c5c852f2344645823f6d671ea1", size = 2489742, upload-time = "2026-04-25T02:10:58.396Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/a38253433e61349f9cba8dcbf849e658c61f9ebf9d3b13833c87b5dac2ef/complexipy-5.4.0-pp311-pypy311_pp73-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c7f2d6812adca07336febe69acb4eac2a642fd7387bab4aba064e0545bc68ac3", size = 2283127, upload-time = "2026-04-25T02:11:00.114Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/c7d725a1b46af4022bc088a5c8ccc55dfa3a033c165f1b3ebd5daecf6a6c/complexipy-5.4.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7fcc410e60c273a1dca4846a79d33e60ff537884263fa1909d157ce05d77bad", size = 2172214, upload-time = "2026-04-25T02:11:01.924Z" }, ] [[package]] @@ -789,68 +788,68 @@ wheels = [ [[package]] name = "greenlet" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/94/a5935717b307d7c71fe877b52b884c6af707d2d2090db118a03fbd799369/greenlet-3.4.0.tar.gz", hash = "sha256:f50a96b64dafd6169e595a5c56c9146ef80333e67d4476a65a9c55f400fc22ff", size = 195913, upload-time = "2026-04-08T17:08:00.863Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/bc/e30e1e3d5e8860b0e0ce4d2b16b2681b77fd13542fc0d72f7e3c22d16eff/greenlet-3.4.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d18eae9a7fb0f499efcd146b8c9750a2e1f6e0e93b5a382b3481875354a430e6", size = 284315, upload-time = "2026-04-08T17:02:52.322Z" }, - { url = "https://files.pythonhosted.org/packages/5b/cc/e023ae1967d2a26737387cac083e99e47f65f58868bd155c4c80c01ec4e0/greenlet-3.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636d2f95c309e35f650e421c23297d5011716be15d966e6328b367c9fc513a82", size = 601916, upload-time = "2026-04-08T16:24:35.533Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/5be1677954b6d8810b33abe94e3eb88726311c58fa777dc97e390f7caf5a/greenlet-3.4.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:234582c20af9742583c3b2ddfbdbb58a756cfff803763ffaae1ac7990a9fac31", size = 616399, upload-time = "2026-04-08T16:30:54.536Z" }, - { url = "https://files.pythonhosted.org/packages/82/0a/3a4af092b09ea02bcda30f33fd7db397619132fe52c6ece24b9363130d34/greenlet-3.4.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ac6a5f618be581e1e0713aecec8e54093c235e5fa17d6d8eb7ffc487e2300508", size = 621077, upload-time = "2026-04-08T16:40:34.946Z" }, - { url = "https://files.pythonhosted.org/packages/74/bf/2d58d5ea515704f83e34699128c9072a34bea27d2b6a556e102105fe62a5/greenlet-3.4.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:523677e69cd4711b5a014e37bc1fb3a29947c3e3a5bb6a527e1cc50312e5a398", size = 611978, upload-time = "2026-04-08T15:56:31.335Z" }, - { url = "https://files.pythonhosted.org/packages/8c/39/3786520a7d5e33ee87b3da2531f589a3882abf686a42a3773183a41ef010/greenlet-3.4.0-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:d336d46878e486de7d9458653c722875547ac8d36a1cff9ffaf4a74a3c1f62eb", size = 416893, upload-time = "2026-04-08T16:43:02.392Z" }, - { url = "https://files.pythonhosted.org/packages/bd/69/6525049b6c179d8a923256304d8387b8bdd4acab1acf0407852463c6d514/greenlet-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b45e45fe47a19051a396abb22e19e7836a59ee6c5a90f3be427343c37908d65b", size = 1571957, upload-time = "2026-04-08T16:26:17.041Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6c/bbfb798b05fec736a0d24dc23e81b45bcee87f45a83cfb39db031853bddc/greenlet-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5434271357be07f3ad0936c312645853b7e689e679e29310e2de09a9ea6c3adf", size = 1637223, upload-time = "2026-04-08T15:57:27.556Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7d/981fe0e7c07bd9d5e7eb18decb8590a11e3955878291f7a7de2e9c668eb7/greenlet-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:a19093fbad824ed7c0f355b5ff4214bffda5f1a7f35f29b31fcaa240cc0135ab", size = 237902, upload-time = "2026-04-08T17:03:14.16Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c6/dba32cab7e3a625b011aa5647486e2d28423a48845a2998c126dd69c85e1/greenlet-3.4.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:805bebb4945094acbab757d34d6e1098be6de8966009ab9ca54f06ff492def58", size = 285504, upload-time = "2026-04-08T15:52:14.071Z" }, - { url = "https://files.pythonhosted.org/packages/54/f4/7cb5c2b1feb9a1f50e038be79980dfa969aa91979e5e3a18fdbcfad2c517/greenlet-3.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:439fc2f12b9b512d9dfa681c5afe5f6b3232c708d13e6f02c845e0d9f4c2d8c6", size = 605476, upload-time = "2026-04-08T16:24:37.064Z" }, - { url = "https://files.pythonhosted.org/packages/d6/af/b66ab0b2f9a4c5a867c136bf66d9599f34f21a1bcca26a2884a29c450bd9/greenlet-3.4.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a70ed1cb0295bee1df57b63bf7f46b4e56a5c93709eea769c1fec1bb23a95875", size = 618336, upload-time = "2026-04-08T16:30:56.59Z" }, - { url = "https://files.pythonhosted.org/packages/6d/31/56c43d2b5de476f77d36ceeec436328533bff960a4cba9a07616e93063ab/greenlet-3.4.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c5696c42e6bb5cfb7c6ff4453789081c66b9b91f061e5e9367fa15792644e76", size = 625045, upload-time = "2026-04-08T16:40:37.111Z" }, - { url = "https://files.pythonhosted.org/packages/e5/5c/8c5633ece6ba611d64bf2770219a98dd439921d6424e4e8cf16b0ac74ea5/greenlet-3.4.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c660bce1940a1acae5f51f0a064f1bc785d07ea16efcb4bc708090afc4d69e83", size = 613515, upload-time = "2026-04-08T15:56:32.478Z" }, - { url = "https://files.pythonhosted.org/packages/80/ca/704d4e2c90acb8bdf7ae593f5cbc95f58e82de95cc540fb75631c1054533/greenlet-3.4.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:89995ce5ddcd2896d89615116dd39b9703bfa0c07b583b85b89bf1b5d6eddf81", size = 419745, upload-time = "2026-04-08T16:43:04.022Z" }, - { url = "https://files.pythonhosted.org/packages/a9/df/950d15bca0d90a0e7395eb777903060504cdb509b7b705631e8fb69ff415/greenlet-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee407d4d1ca9dc632265aee1c8732c4a2d60adff848057cdebfe5fe94eb2c8a2", size = 1574623, upload-time = "2026-04-08T16:26:18.596Z" }, - { url = "https://files.pythonhosted.org/packages/1a/e7/0839afab829fcb7333c9ff6d80c040949510055d2d4d63251f0d1c7c804e/greenlet-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:956215d5e355fffa7c021d168728321fd4d31fd730ac609b1653b450f6a4bc71", size = 1639579, upload-time = "2026-04-08T15:57:29.231Z" }, - { url = "https://files.pythonhosted.org/packages/d9/2b/b4482401e9bcaf9f5c97f67ead38db89c19520ff6d0d6699979c6efcc200/greenlet-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cb614ace7c27571270354e9c9f696554d073f8aa9319079dcba466bbdead711", size = 238233, upload-time = "2026-04-08T17:02:54.286Z" }, - { url = "https://files.pythonhosted.org/packages/0c/4d/d8123a4e0bcd583d5cfc8ddae0bbe29c67aab96711be331a7cc935a35966/greenlet-3.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:04403ac74fe295a361f650818de93be11b5038a78f49ccfb64d3b1be8fbf1267", size = 235045, upload-time = "2026-04-08T17:04:05.072Z" }, - { url = "https://files.pythonhosted.org/packages/65/8b/3669ad3b3f247a791b2b4aceb3aa5a31f5f6817bf547e4e1ff712338145a/greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a", size = 286902, upload-time = "2026-04-08T15:52:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/38/3e/3c0e19b82900873e2d8469b590a6c4b3dfd2b316d0591f1c26b38a4879a5/greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97", size = 606099, upload-time = "2026-04-08T16:24:38.408Z" }, - { url = "https://files.pythonhosted.org/packages/b5/33/99fef65e7754fc76a4ed14794074c38c9ed3394a5bd129d7f61b705f3168/greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996", size = 618837, upload-time = "2026-04-08T16:30:58.298Z" }, - { url = "https://files.pythonhosted.org/packages/44/57/eae2cac10421feae6c0987e3dc106c6d86262b1cb379e171b017aba893a6/greenlet-3.4.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f0def07ec9a71d72315cf26c061aceee53b306c36ed38c35caba952ea1b319d", size = 624901, upload-time = "2026-04-08T16:40:38.981Z" }, - { url = "https://files.pythonhosted.org/packages/36/f7/229f3aed6948faa20e0616a0b8568da22e365ede6a54d7d369058b128afd/greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc", size = 615062, upload-time = "2026-04-08T15:56:33.766Z" }, - { url = "https://files.pythonhosted.org/packages/6a/8a/0e73c9b94f31d1cc257fe79a0eff621674141cdae7d6d00f40de378a1e42/greenlet-3.4.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:0e1254cf0cbaa17b04320c3a78575f29f3c161ef38f59c977108f19ffddaf077", size = 423927, upload-time = "2026-04-08T16:43:05.293Z" }, - { url = "https://files.pythonhosted.org/packages/08/97/d988180011aa40135c46cd0d0cf01dd97f7162bae14139b4a3ef54889ba5/greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de", size = 1573511, upload-time = "2026-04-08T16:26:20.058Z" }, - { url = "https://files.pythonhosted.org/packages/d4/0f/a5a26fe152fb3d12e6a474181f6e9848283504d0afd095f353d85726374b/greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08", size = 1640396, upload-time = "2026-04-08T15:57:30.88Z" }, - { url = "https://files.pythonhosted.org/packages/42/cf/bb2c32d9a100e36ee9f6e38fad6b1e082b8184010cb06259b49e1266ca01/greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2", size = 238892, upload-time = "2026-04-08T17:03:10.094Z" }, - { url = "https://files.pythonhosted.org/packages/b7/47/6c41314bac56e71436ce551c7fbe3cc830ed857e6aa9708dbb9c65142eb6/greenlet-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f38b81880ba28f232f1f675893a39cf7b6db25b31cc0a09bb50787ecf957e85e", size = 235599, upload-time = "2026-04-08T15:52:54.3Z" }, - { url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856, upload-time = "2026-04-08T15:52:45.82Z" }, - { url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208, upload-time = "2026-04-08T16:24:39.674Z" }, - { url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269, upload-time = "2026-04-08T16:30:59.767Z" }, - { url = "https://files.pythonhosted.org/packages/e0/93/c8c508d68ba93232784bbc1b5474d92371f2897dfc6bc281b419f2e0d492/greenlet-3.4.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98eedd1803353daf1cd9ef23eef23eda5a4d22f99b1f998d273a8b78b70dd47f", size = 628455, upload-time = "2026-04-08T16:40:40.698Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549, upload-time = "2026-04-08T15:56:34.893Z" }, - { url = "https://files.pythonhosted.org/packages/7f/46/cfaaa0ade435a60550fd83d07dfd5c41f873a01da17ede5c4cade0b9bab8/greenlet-3.4.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:b7857e2202aae67bc5725e0c1f6403c20a8ff46094ece015e7d474f5f7020b55", size = 426238, upload-time = "2026-04-08T16:43:06.865Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310, upload-time = "2026-04-08T16:26:21.671Z" }, - { url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435, upload-time = "2026-04-08T15:57:32.572Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760, upload-time = "2026-04-08T17:04:03.878Z" }, - { url = "https://files.pythonhosted.org/packages/9b/96/795619651d39c7fbd809a522f881aa6f0ead504cc8201c3a5b789dfaef99/greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a", size = 235498, upload-time = "2026-04-08T17:05:00.584Z" }, - { url = "https://files.pythonhosted.org/packages/78/02/bde66806e8f169cf90b14d02c500c44cdbe02c8e224c9c67bafd1b8cadd1/greenlet-3.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:10a07aca6babdd18c16a3f4f8880acfffc2b88dfe431ad6aa5f5740759d7d75e", size = 286291, upload-time = "2026-04-08T17:09:34.307Z" }, - { url = "https://files.pythonhosted.org/packages/05/1f/39da1c336a87d47c58352fb8a78541ce63d63ae57c5b9dae1fe02801bbc2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:076e21040b3a917d3ce4ad68fb5c3c6b32f1405616c4a57aa83120979649bd3d", size = 656749, upload-time = "2026-04-08T16:24:41.721Z" }, - { url = "https://files.pythonhosted.org/packages/d3/6c/90ee29a4ee27af7aa2e2ec408799eeb69ee3fcc5abcecac6ddd07a5cd0f2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e82689eea4a237e530bb5cb41b180ef81fa2160e1f89422a67be7d90da67f615", size = 669084, upload-time = "2026-04-08T16:31:01.372Z" }, - { url = "https://files.pythonhosted.org/packages/d2/4a/74078d3936712cff6d3c91a930016f476ce4198d84e224fe6d81d3e02880/greenlet-3.4.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:06c2d3b89e0c62ba50bd7adf491b14f39da9e7e701647cb7b9ff4c99bee04b19", size = 673405, upload-time = "2026-04-08T16:40:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/07/49/d4cad6e5381a50947bb973d2f6cf6592621451b09368b8c20d9b8af49c5b/greenlet-3.4.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df3b0b2289ec686d3c821a5fee44259c05cfe824dd5e6e12c8e5f5df23085cf", size = 665621, upload-time = "2026-04-08T15:56:35.995Z" }, - { url = "https://files.pythonhosted.org/packages/79/3e/df8a83ab894751bc31e1106fdfaa80ca9753222f106b04de93faaa55feb7/greenlet-3.4.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:070b8bac2ff3b4d9e0ff36a0d19e42103331d9737e8504747cd1e659f76297bd", size = 471670, upload-time = "2026-04-08T16:43:08.512Z" }, - { url = "https://files.pythonhosted.org/packages/37/31/d1edd54f424761b5d47718822f506b435b6aab2f3f93b465441143ea5119/greenlet-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bff29d586ea415688f4cec96a591fcc3bf762d046a796cdadc1fdb6e7f2d5bf", size = 1622259, upload-time = "2026-04-08T16:26:23.201Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c6/6d3f9cdcb21c4e12a79cb332579f1c6aa1af78eb68059c5a957c7812d95e/greenlet-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a569c2fb840c53c13a2b8967c63621fafbd1a0e015b9c82f408c33d626a2fda", size = 1686916, upload-time = "2026-04-08T15:57:34.282Z" }, - { url = "https://files.pythonhosted.org/packages/63/45/c1ca4a1ad975de4727e52d3ffe641ae23e1d7a8ffaa8ff7a0477e1827b92/greenlet-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:207ba5b97ea8b0b60eb43ffcacf26969dd83726095161d676aac03ff913ee50d", size = 239821, upload-time = "2026-04-08T17:03:48.423Z" }, - { url = "https://files.pythonhosted.org/packages/71/c4/6f621023364d7e85a4769c014c8982f98053246d142420e0328980933ceb/greenlet-3.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:f8296d4e2b92af34ebde81085a01690f26a51eb9ac09a0fcadb331eb36dbc802", size = 236932, upload-time = "2026-04-08T17:04:33.551Z" }, - { url = "https://files.pythonhosted.org/packages/d4/8f/18d72b629783f5e8d045a76f5325c1e938e659a9e4da79c7dcd10169a48d/greenlet-3.4.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d70012e51df2dbbccfaf63a40aaf9b40c8bed37c3e3a38751c926301ce538ece", size = 294681, upload-time = "2026-04-08T15:52:35.778Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ad/5fa86ec46769c4153820d58a04062285b3b9e10ba3d461ee257b68dcbf53/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a58bec0751f43068cd40cff31bb3ca02ad6000b3a51ca81367af4eb5abc480c8", size = 658899, upload-time = "2026-04-08T16:24:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/43/f0/4e8174ca0e87ae748c409f055a1ba161038c43cc0a5a6f1433a26ac2e5bf/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05fa0803561028f4b2e3b490ee41216a842eaee11aed004cc343a996d9523aa2", size = 665284, upload-time = "2026-04-08T16:31:02.833Z" }, - { url = "https://files.pythonhosted.org/packages/ef/92/466b0d9afd44b8af623139a3599d651c7564fa4152f25f117e1ee5949ffb/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c4cd56a9eb7a6444edbc19062f7b6fbc8f287c663b946e3171d899693b1c19fa", size = 665872, upload-time = "2026-04-08T16:40:43.912Z" }, - { url = "https://files.pythonhosted.org/packages/19/da/991cf7cd33662e2df92a1274b7eb4d61769294d38a1bba8a45f31364845e/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e60d38719cb80b3ab5e85f9f1aed4960acfde09868af6762ccb27b260d68f4ed", size = 661861, upload-time = "2026-04-08T15:56:37.269Z" }, - { url = "https://files.pythonhosted.org/packages/0d/14/3395a7ef3e260de0325152ddfe19dffb3e49fe10873b94654352b53ad48e/greenlet-3.4.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:1f85f204c4d54134ae850d401fa435c89cd667d5ce9dc567571776b45941af72", size = 489237, upload-time = "2026-04-08T16:43:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/36/c5/6c2c708e14db3d9caea4b459d8464f58c32047451142fe2cfd90e7458f41/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f50c804733b43eded05ae694691c9aa68bca7d0a867d67d4a3f514742a2d53f", size = 1622182, upload-time = "2026-04-08T16:26:24.777Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4c/50c5fed19378e11a29fabab1f6be39ea95358f4a0a07e115a51ca93385d8/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2d4f0635dc4aa638cda4b2f5a07ae9a2cff9280327b581a3fcb6f317b4fbc38a", size = 1685050, upload-time = "2026-04-08T15:57:36.453Z" }, - { url = "https://files.pythonhosted.org/packages/db/72/85ae954d734703ab48e622c59d4ce35d77ce840c265814af9c078cacc7aa/greenlet-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1a4a48f24681300c640f143ba7c404270e1ebbbcf34331d7104a4ff40f8ea705", size = 245554, upload-time = "2026-04-08T17:03:50.044Z" }, +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/03/84359833f7e1d49a883e92777637c592306030e30cee5e2b1e6476f95c88/greenlet-3.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a", size = 283502, upload-time = "2026-04-27T12:20:55.213Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/6f9f008266273aa14a2e011945797ac5802b97b8b40efe7afe1ee6c1afc9/greenlet-3.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f", size = 600508, upload-time = "2026-04-27T12:52:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/b0f3272c2368ea2c1aa19a5ad70db0be8f8dff6e6d3d1eb82efa00cbcf19/greenlet-3.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb", size = 613283, upload-time = "2026-04-27T12:59:37.957Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ae/1db979ff6ae7958d80b288f63d5f6c30df96682700ea9fc340ce994d94a1/greenlet-3.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd", size = 619894, upload-time = "2026-04-27T13:02:35.13Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ac/0b509b6fb93551ce5a01612ee1acda7f7dda4bbb66c99aeb2ab403d205dc/greenlet-3.5.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb", size = 613418, upload-time = "2026-04-27T12:25:23.852Z" }, + { url = "https://files.pythonhosted.org/packages/ce/94/b0590e3d1978f02419f30502341c40d72f77eb0a2198119fe27df47714ee/greenlet-3.5.0-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243", size = 415681, upload-time = "2026-04-27T13:05:11.494Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/2b2b680ec87aaa97998fb5b8d76658d4d3560386864f17efab33ba7c2e24/greenlet-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977", size = 1572229, upload-time = "2026-04-27T12:53:23.509Z" }, + { url = "https://files.pythonhosted.org/packages/61/e4/42b259e7a19aff1a270a4bd82caf6353109ed6860c9454e18f37162b83ae/greenlet-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0", size = 1639886, upload-time = "2026-04-27T12:25:22.325Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/733ca47b883b67c57f90d3ecb21055c9ec753597d10754ac201644061f9d/greenlet-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858", size = 237795, upload-time = "2026-04-27T12:21:40.118Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0f/a91f143f356523ff682309732b175765a9bc2836fd7c081c2c67fedc1ad4/greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082", size = 284726, upload-time = "2026-04-27T12:20:51.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/800646c7ffc5dbabd75ddd2f6b519bb898c0c9c969e5d0473bfe5d20bcce/greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3", size = 604264, upload-time = "2026-04-27T12:52:39.494Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/354867c0bba812fc33b15bc55aedafedd0aee3c7dd91dfca22444157dc0c/greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c", size = 616099, upload-time = "2026-04-27T12:59:39.623Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ab/192090c4a5b30df148c22bf4b8895457d739a7c7c5a7b9c41e5dd7f537f2/greenlet-3.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564", size = 623976, upload-time = "2026-04-27T13:02:37.363Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/815bece7399e01cadb69014219eebd0042339875c59a59b0820a46ece356/greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662", size = 615198, upload-time = "2026-04-27T12:25:25.928Z" }, + { url = "https://files.pythonhosted.org/packages/24/11/05eb2b9b188c6df7d68a89c99134d644a7af616a40b9808e8e6ced315d5d/greenlet-3.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc", size = 418379, upload-time = "2026-04-27T13:05:12.755Z" }, + { url = "https://files.pythonhosted.org/packages/10/80/3b2c0a895d6698f6ddb31b07942ebfa982f3e30888bc5546a5b5990de8b2/greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b", size = 1574927, upload-time = "2026-04-27T12:53:25.81Z" }, + { url = "https://files.pythonhosted.org/packages/44/0e/f354af514a4c61454dbc68e44d47544a5a4d6317e30b77ddfa3a09f4c5f3/greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4", size = 1642683, upload-time = "2026-04-27T12:25:23.9Z" }, + { url = "https://files.pythonhosted.org/packages/fa/6a/87f38255201e993a1915265ebb80cd7c2c78b04a45744995abbf6b259fd8/greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8", size = 238115, upload-time = "2026-04-27T12:21:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f8/450fe3c5938fa737ea4d22699772e6e34e8e24431a47bf4e8a1ceed4a98e/greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339", size = 235017, upload-time = "2026-04-27T12:22:26.768Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e9/4eeadf8cb3403ac274245ba75f07844abc7fa5f6787583fc9156ba741e0f/greenlet-3.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", size = 620610, upload-time = "2026-04-27T13:02:39.194Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ef/f913b3c0eb7d26d86a2401c5e1546c9d46b657efee724b06f6f4ac5d8824/greenlet-3.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", size = 422775, upload-time = "2026-04-27T13:05:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, + { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234, upload-time = "2026-04-27T13:02:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243, upload-time = "2026-04-27T13:05:15.689Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, + { url = "https://files.pythonhosted.org/packages/fb/89/2dadb89793c37ee8b4c237857188293e9060dc085f19845c292e00f8e091/greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", size = 668086, upload-time = "2026-04-27T13:02:42.314Z" }, + { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/75722be7e26a2af4cbd2dc35b0ed382dacf9394b7e75551f76ed1abe87f2/greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", size = 470799, upload-time = "2026-04-27T13:05:17.094Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, + { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5c/0602239503b124b70e39355cbdb39361ecfe65b87a5f2f63752c32f5286f/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", size = 657015, upload-time = "2026-04-27T13:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/38/51/8699f865f125dc952384cb432b0f7138aa4d8f2969a7d12d0df5b94d054d/greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", size = 488275, upload-time = "2026-04-27T13:05:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, ] [[package]] @@ -887,11 +886,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] [[package]] @@ -1021,6 +1020,19 @@ version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5e/73/e01e4c5e11ad0494f4407a3f623ad4d87714909f50b17a06ed121034ff6e/jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc", size = 13925, upload-time = "2022-01-16T20:35:59.13Z" } +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + [[package]] name = "lxml" version = "6.1.0" @@ -1559,11 +1571,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.1" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -1577,22 +1589,23 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "picopt" -version = "6.2.0" +version = "6.3.0" source = { editable = "." } dependencies = [ { name = "confuse" }, { name = "defusedxml" }, { name = "filetype" }, { name = "humanize" }, + { name = "loguru" }, { name = "mozjpeg-lossless-optimization" }, { name = "piexif" }, { name = "pikepdf" }, @@ -1601,6 +1614,7 @@ dependencies = [ { name = "pyoxipng" }, { name = "python-dateutil" }, { name = "rarfile" }, + { name = "rich" }, { name = "ruamel-yaml" }, { name = "treestamps" }, { name = "typing-extensions" }, @@ -1650,6 +1664,7 @@ requires-dist = [ { name = "defusedxml", specifier = ">=0.7,<1.0" }, { name = "filetype", specifier = "~=1.2" }, { name = "humanize", specifier = "~=4.12" }, + { name = "loguru", specifier = "~=0.7" }, { name = "mozjpeg-lossless-optimization", specifier = "~=1.3" }, { name = "piexif", specifier = "~=1.1" }, { name = "pikepdf", specifier = ">=10.5.1" }, @@ -1658,8 +1673,9 @@ requires-dist = [ { name = "pyoxipng", specifier = "~=9.1" }, { name = "python-dateutil", specifier = "~=2.8" }, { name = "rarfile", specifier = "~=4.0" }, + { name = "rich", specifier = "~=15.0" }, { name = "ruamel-yaml", specifier = ">=0.18,<1.0" }, - { name = "treestamps", specifier = "~=3.0.0" }, + { name = "treestamps", specifier = "~=4.0.1" }, { name = "typing-extensions", specifier = "~=4.13" }, ] @@ -1686,7 +1702,7 @@ lint = [ { name = "complexipy", specifier = "~=5.1" }, { name = "icecream", specifier = "~=2.1" }, { name = "mbake", specifier = "~=1.4.5" }, - { name = "pathspec", specifier = "~=1.0.4" }, + { name = "pathspec", specifier = "~=1.1.0" }, { name = "radon", extras = ["toml"], specifier = "~=6.0" }, { name = "ruff", specifier = ">=0.13,<1.0" }, { name = "ty", specifier = ">=0.0.4,<1.0" }, @@ -2523,27 +2539,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, - { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, - { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, - { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, - { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, - { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, - { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, - { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, - { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, - { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, - { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] [[package]] @@ -2573,15 +2589,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "termcolor" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, -] - [[package]] name = "texttable" version = "1.7.0" @@ -2671,45 +2678,44 @@ wheels = [ [[package]] name = "treestamps" -version = "3.0.1" +version = "4.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml" }, - { name = "termcolor" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/fb/fc0c13d3b4dbed1badae7f20bdf13e6455d3ffba7120bc041d8fee6c80e9/treestamps-3.0.1.tar.gz", hash = "sha256:b292ae74c55849bad5aa76cc04f943f345fa49e0aa25f08ca735cddfd7d8c1b4", size = 182154, upload-time = "2026-04-14T23:18:40.689Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9f/af4d53d7092f0ba7800bc3827ce8597a82f7879d2de591b2205eeb13d4d6/treestamps-3.0.1-py3-none-any.whl", hash = "sha256:83aa1837111341bfeb9c527fd9b1f12752891c10f79d0c076bf2b37cd5d8a556", size = 16280, upload-time = "2026-04-14T23:18:39.525Z" }, + { 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" }, ] [[package]] name = "ty" -version = "0.0.32" +version = "0.0.33" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/7e/2aa791c9ae7b8cd5024cd4122e92267f664ca954cea3def3211919fa3c1f/ty-0.0.32.tar.gz", hash = "sha256:8743174c5f920f6700a4a0c9de140109189192ba16226884cd50095b43b8a45c", size = 5522294, upload-time = "2026-04-20T19:29:01.626Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/44/9478c50c266826c1bf30d1692e589755bffa8f1c0a3eb7af8a346c255991/ty-0.0.33.tar.gz", hash = "sha256:46d63bda07403322cb6c28ccfdd5536be916e13df725c29f7ccd0a21f06bd9e8", size = 5559373, upload-time = "2026-04-28T10:45:13.18Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/eb/1075dc6a49d7acbe2584ae4d5b410c41b1f177a5adcc567e09eca4c69000/ty-0.0.32-py3-none-linux_armv6l.whl", hash = "sha256:dacbc2f6cd698d488ae7436838ff929570455bf94bfa4d9fe57a630c552aff83", size = 10902959, upload-time = "2026-04-20T19:28:31.907Z" }, - { url = "https://files.pythonhosted.org/packages/33/d2/c35fc8bc66e98d1ee9b0f8ed319bf743e450e1f1e997574b178fab75670f/ty-0.0.32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914bbc4f605ce2a9e2a78982e28fae1d3359a169d141f9dc3b4c7749cd5eca81", size = 10726172, upload-time = "2026-04-20T19:28:44.765Z" }, - { url = "https://files.pythonhosted.org/packages/96/32/c827da3ca480456fb02d8cea68a2609273b6c220fea0be9a4c8d8470b86e/ty-0.0.32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4787ac9fe1f86b1f3133f5c6732adbe2df5668b50c679ac6e2d98cd284da812f", size = 10163701, upload-time = "2026-04-20T19:28:27.005Z" }, - { url = "https://files.pythonhosted.org/packages/ba/9e/2734478fbdb90c160cb2813a3916a16a2af5c1e231f87d635f6131d781fb/ty-0.0.32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ea0a728af99fe40dd744cba6441a2404f80b7f4bde17aa6da393810af5ea57", size = 10656220, upload-time = "2026-04-20T19:29:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/44/9f/0007da2d35e424debe7e9f86ffbc1ab7f60983cfbc5f0411324ab2de5292/ty-0.0.32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2850561f9b018ae33d7e5bbfa0ac414d3c518513edcffe43877dc9801446b9c5", size = 10696086, upload-time = "2026-04-20T19:28:46.829Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5e/ce5fd4ec803222ae3e69a76d2a2db2eed55e19f5b131702b9789ef45f93d/ty-0.0.32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5fa2fb3c614349ee211d36476b49d88c5ef79a687cdb91b2872ad023b94d2f8", size = 11184800, upload-time = "2026-04-20T19:28:42.57Z" }, - { url = "https://files.pythonhosted.org/packages/6c/46/ebcf67a5999421331214aac51a7464db42de2be15bbe929c612a3ed0b039/ty-0.0.32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b89969307ab2417d41c9be8059dd79feea577234e1e10d35132f5495e0d42c6", size = 11718718, upload-time = "2026-04-20T19:28:36.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/2c/2141c86ed0ce0962b45cefb658a95e734f59759d47f20afdcd9c732910a1/ty-0.0.32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b59868ede9b1d69a088f0d695df52a0061f95fa7baa1d5e0dc6fc9cf06e1334", size = 11346369, upload-time = "2026-04-20T19:28:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/7a/da/ed6f772339cf29bd9a46def9d6db5084689eb574ee4d150ff704224c1ed8/ty-0.0.32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8300caf35345498e9b9b03e550bba03cee8f5f5f8ab4c83c3b1ff1b7403b7d3a", size = 11280714, upload-time = "2026-04-20T19:28:51.516Z" }, - { url = "https://files.pythonhosted.org/packages/da/9b/c6813987edf4816a40e0c8e408b555f97d3f267c7b3a1688c8bbdf65609c/ty-0.0.32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:583c7094f4574b02f724db924f98b804d1387a0bd9405ecb5e078cc0f47fbcfb", size = 10638806, upload-time = "2026-04-20T19:28:29.651Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d4/0cefcbd2ad0f3d51762ccf58e652ec7da146eb6ae34f87228f6254bbb8be/ty-0.0.32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e44ebe1bb4143a5628bc4db67ac0dfebe14594af671e4ee66f6f2e983da56501", size = 10726106, upload-time = "2026-04-20T19:29:06.3Z" }, - { url = "https://files.pythonhosted.org/packages/32/ad/2c8a97f91f06311f4367400f7d13534bbda2522c73c99a3e4c0757dff9b8/ty-0.0.32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:06f17ada3e069cba6148342ef88e9929156beca8473e8d4f101b68f66c75643e", size = 10872951, upload-time = "2026-04-20T19:28:34.077Z" }, - { url = "https://files.pythonhosted.org/packages/ba/68/42293f9248106dd51875120971a5cc6ea315c2c4dcfb8e59aa063aa0af26/ty-0.0.32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e96e60fa556cec04f15d7ea62d2ceee5982bd389233e961ab9fd42304e278175", size = 11363334, upload-time = "2026-04-20T19:28:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/df/92/be9abf4d3e589ad5023e2ea965b93e204ec856420d46adf73c5c36c04678/ty-0.0.32-py3-none-win32.whl", hash = "sha256:2ff2ebb4986b24aebcf1444db7db5ca41b36086040e95eea9f8fb851c11e805c", size = 10260689, upload-time = "2026-04-20T19:28:56.541Z" }, - { url = "https://files.pythonhosted.org/packages/14/61/dc86acea899349d2579cb8419aecedd83dc504d7d6a10df65eef546c8300/ty-0.0.32-py3-none-win_amd64.whl", hash = "sha256:ba7284a4a954b598c1b31500352b3ec1f89bff533825592b5958848226fdc7ee", size = 11255371, upload-time = "2026-04-20T19:28:39.917Z" }, - { url = "https://files.pythonhosted.org/packages/43/01/beffec56d71ca25b343ede63adb076456b5b3e211f1c066452a44cd120b3/ty-0.0.32-py3-none-win_arm64.whl", hash = "sha256:7e10aadbdbda989a7d567ee6a37f8b98d4d542e31e3b190a2879fd581f75d658", size = 10658087, upload-time = "2026-04-20T19:28:59.286Z" }, + { url = "https://files.pythonhosted.org/packages/e9/24/e287388c63a19191be26b32ff4dbd06029834068150ebe2532939bc4c851/ty-0.0.33-py3-none-linux_armv6l.whl", hash = "sha256:94d0a9d2234261a8911396d59e506b5923fe0971dbda43b9dcea287936887fcc", size = 11021308, upload-time = "2026-04-28T10:45:43.34Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/ba1eed819895bd239fba8ee35dfcd5fcb266c203b0914a17a59579096bb5/ty-0.0.33-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4a2b5ba078f90de342f56b5f7979bb77c9b9b1d8625a041352ffc6ee93c4073", size = 10777272, upload-time = "2026-04-28T10:45:32.905Z" }, + { url = "https://files.pythonhosted.org/packages/25/a8/c3131d37b44b3fea1d6654a1c929a0cd0873822f77a90482b8ec28f6fbbd/ty-0.0.33-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84ff5707825e9af9668d2bcf66975f93e520a63b524ab494e3a8265735be2563", size = 10201078, upload-time = "2026-04-28T10:45:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/7b/db/d8e37ff0045810cc65e1ff36aa0da0a2253c05659787ac987df8a16c7897/ty-0.0.33-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e375285736f57886868e7af0b11c7b0ec5b6543fa15e7ad2a714fed9f077d4e0", size = 10732347, upload-time = "2026-04-28T10:45:21.444Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1a/20e83a412506a918e4684fc67b567cf7cc13b105470b3428cb23c3d5aa13/ty-0.0.33-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5680f6350c3b4e46b8bff6d7bb132366ea239463d6cad4892725d06046e65464", size = 10808238, upload-time = "2026-04-28T10:45:38.565Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4b/d0a39f4464dc6cb4cc2c159473ce216bd1846bfb684c0323a3cb36dce5c6/ty-0.0.33-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5535538bad8d0f7e62bcdff02197cdb30e41451d80b35d27e17d128f2e1dc5d", size = 11288348, upload-time = "2026-04-28T10:45:08.419Z" }, + { url = "https://files.pythonhosted.org/packages/35/7e/f1745e0f9583363d7a83d9a4990fc244f76ecc30840ddad83dc16a33c52d/ty-0.0.33-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da196c42bbbc069e1e21e3e52107c061aa9660352dae57a41930690b56e2c02d", size = 11789907, upload-time = "2026-04-28T10:45:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/25f39f46a12d662859d45bc648555d0661044eb43db6b5648c9947487da9/ty-0.0.33-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9281672921ef6d4460e03146b5e6c18cb1a3e3a3b8a1a88f6f33226d05a469b7", size = 11500774, upload-time = "2026-04-28T10:45:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/94/ec/136959ecbb7c71cb90537f5aea441c73f4ab24612868a6ecdc9d7444d32d/ty-0.0.33-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c1b8f303f82da64e878108e764be3ecbcd7c9903ac0a7f7031614ed00b97ab", size = 11360314, upload-time = "2026-04-28T10:45:05.402Z" }, + { url = "https://files.pythonhosted.org/packages/cf/95/32809575c222f00beed498cb728e9290a0f5009f930025381bb7253b2206/ty-0.0.33-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:efe3af412c9ff67bce5fa37d0a2b0d8555c24072b145a5bac6c79637f1c83abe", size = 10707785, upload-time = "2026-04-28T10:45:10.836Z" }, + { url = "https://files.pythonhosted.org/packages/13/89/c8e9531f7aa4a093359e15fa32c8e1277fbbe90d16894d7c6032d29f4b34/ty-0.0.33-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aeec29c91ea768601747da546c3efc20b72c2fb1bd52bcc786a5c6eeff51d27b", size = 10834987, upload-time = "2026-04-28T10:45:40.738Z" }, + { url = "https://files.pythonhosted.org/packages/31/16/9835fbcf5338af1a1917bd28fdb8a7193c210b83f243aa286fa9f79cb3ad/ty-0.0.33-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a535977c52bbb5f7e96b8b70a6ad375ad077f4a9ff2492508ea3816a2b403819", size = 10968968, upload-time = "2026-04-28T10:45:30.26Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/64c76aabc1bc70c7f24b686cd93c3407f8ea430905e395f59bf9603ef571/ty-0.0.33-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1d732facf39fcb221ba279d469c5040d37883e964f123b1563888efd34818180", size = 11458077, upload-time = "2026-04-28T10:45:45.971Z" }, + { url = "https://files.pythonhosted.org/packages/91/84/fae27b0c4718776a298690d31ca4cc1995f2e3e1c63a7b59e84c41498e9a/ty-0.0.33-py3-none-win32.whl", hash = "sha256:d90960b574428dc252f85e8598ec5fcb7f619794196b2fc95a90da075ed4681c", size = 10345364, upload-time = "2026-04-28T10:45:16.836Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a0/a2938b23ae3e1a09a2d7c189e2ac5f7113676bae4e0e23948b568e18e5f8/ty-0.0.33-py3-none-win_amd64.whl", hash = "sha256:c1c3aec62c44de610c6e95f0a4e97ac3dbc07934bfdbf1fd90d758c9ff72f48e", size = 11342470, upload-time = "2026-04-28T10:45:26.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/62/7fb948aace38d2f6329261bb33c035a8484549c74f1db28649c7a4c6fed9/ty-0.0.33-py3-none-win_arm64.whl", hash = "sha256:0d44f99ba1b441e55e2aa301b2ac0a21112784931b46a5f66f4ea9efe5620d97", size = 10742673, upload-time = "2026-04-28T10:45:35.555Z" }, ] [[package]] name = "typer" -version = "0.24.1" +version = "0.25.0" 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/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/27/ede8cec7596e0041ba7e7b80b47d132562f56ff454313a16f6084e555c9f/typer-0.25.0.tar.gz", hash = "sha256:123eaf9f19bb40fd268310e12a542c0c6b4fab9c98d9d23342a01ff95e3ce930", size = 120150, upload-time = "2026-04-26T08:46:14.767Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-py3-none-any.whl", hash = "sha256:ac01b48823d3db9a83c9e164338057eadbb1c9957a2a6b4eeb486669c560b5dc", size = 55993, upload-time = "2026-04-26T08:46:15.889Z" }, ] [[package]] @@ -2806,6 +2812,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +] + [[package]] name = "wrapt" version = "2.1.2"