diff --git a/.gitignore b/.gitignore index 9e921e7..b8a0f13 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ sbackup.json .venv .qwen -memory \ No newline at end of file +memory +.coverage \ No newline at end of file diff --git a/README.md b/README.md index 32b54a2..5e4058d 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,15 @@ Sbackup 是一个轻量级的文件夹备份工具,支持通过命令行添加 ## 功能特性 - ✅ **增量备份**:仅备份已更改的文件夹,节省时间和存储空间 +- ✅ **多格式支持**:支持 ZIP、tar、tar.gz、tar.bz2、tar.xz、tar.zst、7z 七种打包格式,全局和条目级均可独立指定 +- ✅ **备份还原**:支持从备份文件解压还原到指定目录 +- ✅ **备份清理**:自动删除旧备份,仅保留最近 N 个文件 +- ✅ **加密备份**:支持 7z 格式密码加密 +- ✅ **定时备份**:设置间隔自动执行,实现无人值守 +- ✅ **备份历史**:记录每次备份的时间、大小和文件数,方便追溯 - ✅ **灵活的策略管理**:支持添加、删除和查看备份策略 - ✅ **自定义配置**:支持通过 `config.json` 自定义压缩算法、忽略模式等 +- ✅ **国际化**:支持中文、英语、法语、西班牙语、俄语、德语、日语、葡萄牙语、韩语九种语言,可随时切换 - ✅ **轻量高效**:体积小,启动速度快,资源占用低 - ✅ **跨平台支持**:支持 Windows、macOS 和 Linux @@ -37,18 +44,18 @@ Sbackup 是一个轻量级的文件夹备份工具,支持通过命令行添加 ### 安装 -#### 使用 pip 安装 +#### 使用 uv 安装 ```bash -pip install sbackup +uv pip install sbackup ``` #### 从源码安装 ```bash -git clone https://github.com/yourusername/sbackup.git +git clone https://gitee.com/xiatianxuan/sbackup.git cd sbackup -pip install -e . +uv sync ``` ### 使用方法 @@ -56,7 +63,7 @@ pip install -e . #### 基本语法 ```bash -sbackup +uv run python main.py [options] ``` #### 可用命令 @@ -67,29 +74,47 @@ sbackup | `rm` 或 `remove` | 删除备份策略 | | `all` | 查看所有备份策略 | | `save` | 执行备份 | +| `watch` | 定时执行备份 | +| `restore` | 从备份文件还原 | | `version` | 查看版本信息 | | `help` | 查看帮助信息 | +#### 全局参数 + +| 参数 | 描述 | +|------|------| +| `--lang zh_CN` / `en_US` / `fr_FR` / `es_ES` / `ru_RU` / `de_DE` / `ja_JP` / `pt_BR` / `ko_KR` | 设置界面语言(持久化到 config.json) | +| `--format zip` / `tar` / `tar.gz` / `tar.bz2` / `tar.xz` / `tar.zst` / `7z` | 设置打包格式(持久化到 config.json) | +| `--debug` | 开启调试日志 | + #### 添加备份策略 ```bash -sbackup add [-i ignore_patterns] +uv run python main.py add [-i ignore_patterns] ``` 参数说明: - **source**:需要备份的源文件夹路径 - **dest**:备份文件存放的目标路径 - **-i, --ignore**:需要忽略的文件或文件夹名称,使用逗号分隔(默认:`.git,__pycache__`) +- **--format**:条目级打包格式(仅作用于该备份策略,不指定则使用全局默认):`zip` / `tar` / `tar.gz` / `tar.bz2` / `tar.xz` / `tar.zst` / `7z` 示例: ```bash -sbackup add F:/my_folder F:/backup -i node_modules,.git +# 使用全局默认格式添加策略 +uv run python main.py add F:/my_folder F:/backup -i node_modules,.git + +# 为该策略指定 tar.gz 格式(每次备份此文件夹都使用 tar.gz) +uv run python main.py add F:/my_folder F:/backup --format tar.gz + +# 指定 7z 格式(仅此文件夹) +uv run python main.py add F:/my_folder F:/backup --format 7z ``` #### 删除备份策略 ```bash -sbackup rm +uv run python main.py rm ``` 参数说明: @@ -97,13 +122,13 @@ sbackup rm 示例: ```bash -sbackup rm F:/my_folder +uv run python main.py rm F:/my_folder ``` #### 查看所有备份策略 ```bash -sbackup all +uv run python main.py all ``` 显示当前所有已配置的备份策略。 @@ -111,15 +136,75 @@ sbackup all #### 执行备份 ```bash -sbackup save +# 使用默认格式(ZIP) +uv run python main.py save + +# 使用 tar.gz 格式 +uv run python main.py --format tar.gz save + +# 保留最近 5 个备份文件,自动清理旧的 +uv run python main.py save --keep 5 + +# 使用 7z 格式并加密 +uv run python main.py --format 7z save --password mysecret + +# 英文界面 + tar.xz 格式 +uv run python main.py --lang en_US --format tar.xz save ``` +**save 命令参数:** + +| 参数 | 默认值 | 描述 | +|------|--------|------| +| `--keep N` | `0` | 保留最近 N 个备份文件,0 表示不清理 | +| `--password PASSWORD` | `""` | 加密密码(仅 7z 格式支持) | + 根据备份策略,自动备份已更改的文件夹。 +#### 定时备份 + +```bash +# 每 60 分钟执行一次备份 +uv run python main.py watch --interval 60 + +# 每 2 小时备份一次,保留最近 10 个文件 +uv run python main.py watch --interval 120 --keep 10 + +# 定时备份 + 7z 加密 +uv run python main.py --format 7z watch --interval 60 --password mysecret +``` + +**watch 命令参数:** + +| 参数 | 默认值 | 描述 | +|------|--------|------| +| `--interval MINUTES` | `60` | 备份间隔(分钟) | +| `--keep N` | `0` | 保留最近 N 个备份文件 | +| `--password PASSWORD` | `""` | 加密密码(仅 7z 格式支持) | + +按 `Ctrl+C` 停止定时备份。 + +#### 还原备份 + +```bash +uv run python main.py restore +``` + +参数说明: +- **backup_file**:备份文件路径(支持 .zip / .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .7z) +- **target_dir**:还原目标目录 + +示例: +```bash +uv run python main.py restore F:/backup/my_folder.tar.gz F:/restored +uv run python main.py restore F:/backup/my_folder.7z F:/restored +uv run python main.py restore F:/backup/my_folder.tar.zst F:/restored +``` + #### 查看版本信息 ```bash -sbackup version +uv run python main.py version ``` ## 配置文件 @@ -130,63 +215,109 @@ Sbackup 支持通过 `config.json` 文件进行自定义配置。配置文件应 ```json { + "compression_format": "ZIP", "compression": { "algorithm": "ZIP_DEFLATED", "level": 6 }, "skip_patterns": [".git", "__pycache__"], - "data_file": "sbackup.json" + "data_file": "sbackup.json", + "lang": "zh_CN", + "password": "" } ``` | 配置项 | 类型 | 默认值 | 描述 | |--------|------|--------|------| -| `compression.algorithm` | string | `"ZIP_DEFLATED"` | 压缩算法,可选值:`ZIP_DEFLATED`, `ZIP_STORED`, `ZIP_BZIP2`, `ZIP_LZMA` | +| `compression_format` | string | `"ZIP"` | 打包格式,可选值:`ZIP`, `TAR`, `TAR_GZ`, `TAR_BZ2`, `TAR_XZ`, `TAR_ZST`, `7Z` | +| `compression.algorithm` | string | `"ZIP_DEFLATED"` | ZIP 压缩算法,可选值:`ZIP_DEFLATED`, `ZIP_STORED`, `ZIP_BZIP2`, `ZIP_LZMA` | | `compression.level` | int | `6` | 压缩级别,范围 0-9(0 为不压缩,9 为最高压缩) | -| `skip_patterns` | list | `[".git", "__pycache__"]` | 需要忽略的文件或文件夹模式 | -| `data_file` | string | `"sbackup.json"` | 备份策略数据文件的存放路径 | +| `skip_patterns` | list | `[".git", "__pycache__"]` | 需要忽略的文件或文件夹模式(支持 fnmatch 通配符和路径匹配) | +| `data_file` | string | 平台默认路径 | 备份策略数据文件的存放路径 | +| `lang` | string | `"zh_CN"` | 界面语言,可选值:`zh_CN`, `en_US`, `fr_FR`, `es_ES`, `ru_RU`, `de_DE`, `ja_JP`, `pt_BR`, `ko_KR` | +| `password` | string | `""` | 7z 加密密码 | ### 示例配置 -使用 `ZIP_BZIP2` 算法进行压缩: +使用 tar.bz2 格式进行高压缩率备份: ```json { - "compression": { - "algorithm": "ZIP_BZIP2", - "level": 9 - }, + "compression_format": "TAR_BZ2", + "compression_level": 9, "skip_patterns": [".git", "__pycache__", "node_modules", "*.log"], - "data_file": "backup_strategies.json" + "data_file": "backup_strategies.json", + "lang": "zh_CN" } ``` +### 打包格式对比 + +| 格式 | 扩展名 | 压缩率 | 速度 | 依赖 | 适用场景 | +|------|--------|--------|------|------|----------| +| ZIP | .zip | 中 | 快 | 标准库 | 通用,Windows 兼容性最好 | +| tar | .tar | 无 | 极快 | 标准库 | 纯归档,配合外部压缩 | +| tar.gz | .tar.gz | 中 | 快 | 标准库 | Linux/macOS 通用 | +| tar.bz2 | .tar.bz2 | 高 | 中 | 标准库 | 高压缩率归档 | +| tar.xz | .tar.xz | 最高 | 慢 | 标准库 | 长期归档,空间敏感 | +| tar.zst | .tar.zst | 中高 | 极快 | zstandard | 现代场景,速度与压缩率平衡 | +| 7z | .7z | 极高 | 慢 | py7zr | 最高压缩率,支持加密 | + ## 实现原理 Sbackup 通过以下方式实现备份功能: -1. **备份策略存储**:备份策略存储在 `sbackup.json` 文件中,包含文件夹路径、最后修改时间、目标路径和忽略模式。 +1. **备份策略存储**:备份策略存储在 JSON 文件中,包含文件夹路径、最后修改时间、目标路径、忽略模式和条目级打包格式。 2. **增量备份**:通过比较文件夹的最后修改时间,仅备份已更改的文件夹。 -3. **压缩备份**:使用 Python 内置的 `zipfile` 模块进行压缩,支持多种压缩算法。 +3. **多格式压缩**:使用 Python 内置的 `zipfile` 和 `tarfile` 模块,以及 `zstandard` 和 `py7zr` 第三方库,支持 7 种打包格式。 +4. **条目级格式**:每个备份策略可指定独立的打包格式(`add --format`),优先于全局 `--format` 设置;未指定时使用全局默认。 +5. **备份清理**:备份成功后自动扫描目标目录,按修改时间排序,删除超出保留数量的旧文件。 +6. **加密备份**:7z 格式支持 LZMA2 加密,通过 `--password` 参数或 `config.json` 配置。 +7. **定时备份**:`watch` 命令在循环中按指定间隔执行备份,`Ctrl+C` 安全退出。 +8. **备份历史**:每次备份后记录时间戳、文件大小和文件数量,保留最近 100 条记录。 ### 数据文件格式 ```json { "/path/to/source/folder": [ - 1719235200.0, // 最后修改时间戳 - "/path/to/target/folder", // 目标备份路径 - [".git", "__pycache__"] // 忽略模式 + 1719235200.0, + "/path/to/target/folder", + [".git", "__pycache__"], + "" + ], + "/path/to/another/folder": [ + 1719235200.0, + "/path/to/another/target", + [".git"], + "TAR_GZ" + ], + "_history": [ + { + "time": "2026-05-01T12:00:00", + "source": "/path/to/source/folder", + "size_mb": 12.5, + "files_count": 150 + } ] } ``` +每个备份策略条目为 4 元素列表:`[mtime, target, skip_patterns, compression_format]` + +| 字段 | 说明 | +|------|------| +| `mtime` | 源文件夹最后修改时间(用于增量备份判断) | +| `target` | 备份文件存放的目标路径 | +| `skip_patterns` | 需忽略的文件/文件夹模式列表 | +| `compression_format` | 条目级打包格式(空字符串表示使用全局默认) | + ## 开发指南 ### 运行测试 ```bash -python -m unittest discover -s tests -t . +uv run coverage run -m unittest discover -s tests -t . && uv run coverage report -m ``` ### 代码结构 @@ -198,8 +329,8 @@ sbackup/ │ ├── __init__.py # CLI 参数解析和命令分发 │ ├── __main__.py # python -m sbackup 入口 │ ├── config.py # 配置加载、语言持久化、数据路径 -│ ├── compression.py # ZIP 压缩功能实现 -│ ├── auto_save.py # 备份策略管理 +│ ├── compression.py # ZIP / TAR 压缩功能实现 +│ ├── auto_save.py # 备份策略管理 + 备份历史 │ └── i18n.py # 国际化支持 ├── tests/ │ └── sbackup/ @@ -223,16 +354,32 @@ sbackup/ ### Q: 备份策略文件被误删了怎么办? -A: 备份策略存储在 `sbackup.json` 文件中。如果误删,可以通过重新运行 `sbackup add` 命令重新添加备份策略。 +A: 备份策略存储在数据文件中。如果误删,可以通过重新运行 `add` 命令重新添加备份策略。 ### Q: 如何修改已添加的备份策略? -A: 目前不支持直接修改备份策略。你可以先使用 `sbackup rm` 删除旧的策略,再使用 `sbackup add` 添加新的策略。 +A: 目前不支持直接修改备份策略。你可以先使用 `rm` 删除旧的策略,再使用 `add` 添加新的策略。 ### Q: 支持远程备份吗? A: 目前仅支持本地备份。远程备份功能正在开发中。 +### Q: tar.gz 和 ZIP 有什么区别? + +A: tar.gz 在 Linux/macOS 上更常用,压缩率略高;ZIP 在 Windows 上更通用,兼容性最好。tar.bz2 和 tar.xz 提供更高的压缩率但速度较慢。tar.zst 是现代算法,速度极快且压缩率不错。7z 压缩率最高且支持加密。 + +### Q: 如何加密备份? + +A: 使用 7z 格式并设置密码:`uv run python main.py --format 7z save --password yourpassword`。密码也可以写入 `config.json` 的 `password` 字段。 + +### Q: 如何自动清理旧备份? + +A: 使用 `--keep` 参数:`uv run python main.py save --keep 5` 只保留最近 5 个备份文件。定时备份时同样支持:`uv run python main.py watch --interval 60 --keep 10`。 + +### Q: 如何设置定时备份? + +A: 使用 `watch` 命令:`uv run python main.py watch --interval 60` 每 60 分钟备份一次。按 `Ctrl+C` 停止。 + ## 贡献指南 欢迎提交 Issue 和 Pull Request! @@ -271,4 +418,4 @@ A: 目前仅支持本地备份。远程备份功能正在开发中。 --- -*最后更新:2026年4月25日* \ No newline at end of file +*最后更新:2026年5月1日* \ No newline at end of file diff --git a/config.json b/config.json index 111f559..f1da15f 100644 --- a/config.json +++ b/config.json @@ -1,3 +1,4 @@ { - "lang": "en_US" + "lang": "zh_CN", + "compression_format": "zip" } \ No newline at end of file diff --git a/main.py b/main.py index 071b8c4..f48a5ef 100644 --- a/main.py +++ b/main.py @@ -14,4 +14,3 @@ def main() -> None: if __name__ == "__main__": main() - diff --git a/pyproject.toml b/pyproject.toml index b48ee80..4061928 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,9 @@ classifiers = [ "Topic :: System :: Archiving :: Backup", ] dependencies = [ + "py7zr>=1.1.0", "tqdm>=4.67.3", + "zstandard>=0.25.0", ] [project.scripts] @@ -27,3 +29,8 @@ sbackup = "sbackup:run" [project.urls] Homepage = "https://gitee.com/xiatianxuan" Repository = "https://gitee.com/xiatianxuan/sbackup" + +[dependency-groups] +dev = [ + "coverage>=7.13.5", +] diff --git a/sbackup/__init__.py b/sbackup/__init__.py index a38e8cb..bd44387 100644 --- a/sbackup/__init__.py +++ b/sbackup/__init__.py @@ -1,113 +1,145 @@ -import sys import os +import sys import argparse import logging +from typing import NoReturn from sbackup.auto_save import BackupManager from sbackup.i18n import set_locale, t -from sbackup.config import load_config, save_lang +from sbackup.config import load_config, save_lang, save_format +from sbackup.compression import restore_backup VERSION = "1.0.0" logger = logging.getLogger(__name__) -EXAMPLES = """示例: - 添加备份策略: - sbackup add F:/my_folder F:/backup -i node_modules,.git - - 删除备份策略: - sbackup rm F:/my_folder - - 查看所有策略: - sbackup all - - 执行备份任务: - sbackup save - - 使用英文界面: - sbackup --lang en_US save -""" + +class LocalizedArgumentParser(argparse.ArgumentParser): + """本地化错误输出的 ArgumentParser 子类""" + + def error(self, message: str) -> NoReturn: + # 将 argparse 生成的英文错误关键词替换为本地化文本 + localized = message + localized = localized.replace( + "invalid choice: ", t("err.argparse.invalid_choice") + ) + localized = localized.replace("choose from", t("err.argparse.choose_from")) + localized = localized.replace( + "invalid float value: ", t("err.argparse.invalid_float") + ) + localized = localized.replace( + "invalid int value: ", t("err.argparse.invalid_int") + ) + localized = localized.replace( + "unrecognized arguments: ", t("err.argparse.unrecognized_args") + ) + localized = localized.replace("required", t("err.argparse.required")) + self.print_usage(sys.stderr) + sys.stderr.write(f"{self.prog}: {localized}\n") + sys.exit(2) + + +def _detect_lang_from_argv() -> str | None: + """从 sys.argv 中提取 --lang 参数值""" + for i, arg in enumerate(sys.argv): + if arg == "--lang" and i + 1 < len(sys.argv): + return sys.argv[i + 1] + if arg.startswith("--lang="): + return arg.split("=", 1)[1] + return None + def get_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( + parser = LocalizedArgumentParser( prog="sbackup", - description=f"""Sbackup v{VERSION} — 轻量级增量备份工具 - -Sbackup 帮助您轻松管理多文件夹的备份任务。它采用增量备份策略, -仅对已修改的文件进行压缩,支持自定义忽略规则,并提供直观的进度反馈。 - -可用命令: - add 添加新的备份策略 (源目录 -> 目标目录) - rm 删除现有的备份策略 - all 显示所有已配置的备份策略详情 - save 根据策略执行备份任务 - version 查看版本信息""", - epilog=EXAMPLES, + description=t("cli.description", version=VERSION), + epilog=t("cli.epilog"), formatter_class=argparse.RawDescriptionHelpFormatter, - add_help=False + add_help=False, ) - # 全局参数 - parser.add_argument("--debug", action="store_true", help="开启调试模式,输出详细的运行日志和状态信息") - parser.add_argument("-h", "--help", action="help", help="显示此帮助信息并退出") - parser.add_argument("--lang", default=None, help="设置界面语言: zh_CN (默认) 或 en_US") - - subparsers = parser.add_subparsers(dest="command", help="选择要执行的命令") + parser.add_argument("--debug", action="store_true", help=t("cli.help.debug")) + parser.add_argument("-h", "--help", action="help", help=t("cli.help.help")) + parser.add_argument("--lang", default=None, help=t("cli.help.lang")) + parser.add_argument( + "--format", + default=None, + choices=["zip", "tar", "tar.gz", "tar.bz2", "tar.xz", "tar.zst", "7z"], + help=t("cli.help.format"), + ) - # add command - add_parser = subparsers.add_parser("add", help="添加新的备份策略") - add_parser.add_argument("source", help="源文件夹路径: 需要被备份的目录") - add_parser.add_argument("dest", help="目标文件夹路径: 存放生成的 .zip 备份文件的目录") - add_parser.add_argument("-i", "--ignore", default=".git,__pycache__", help="需要忽略的文件或文件夹名称 (使用逗号分隔,默认: .git,__pycache__)") + subparsers = parser.add_subparsers(dest="command", help=t("cli.help.subcommands")) - # rm command - rm_parser = subparsers.add_parser("rm", aliases=["remove"], help="删除备份策略") - rm_parser.add_argument("path", help="需要删除备份策略的源文件夹路径") + add_parser = subparsers.add_parser("add", help=t("cli.help.add")) + add_parser.add_argument("source", help=t("cli.help.add.source")) + add_parser.add_argument("dest", help=t("cli.help.add.dest")) + add_parser.add_argument( + "-i", "--ignore", default=".git,__pycache__", help=t("cli.help.add.ignore") + ) + add_parser.add_argument( + "--format", + default=None, + choices=["zip", "tar", "tar.gz", "tar.bz2", "tar.xz", "tar.zst", "7z"], + help=t("cli.help.add.format"), + ) - # all command - subparsers.add_parser("all", help="查看所有备份策略") + rm_parser = subparsers.add_parser("rm", aliases=["remove"], help=t("cli.help.rm")) + rm_parser.add_argument("path", help=t("cli.help.rm.path")) - # save command - subparsers.add_parser("save", help="执行所有备份策略") + subparsers.add_parser("all", help=t("cli.help.all")) - # version command - subparsers.add_parser("version", help="查看版本信息") + save_parser = subparsers.add_parser("save", help=t("cli.help.save")) + save_parser.add_argument( + "--keep", type=int, default=0, help=t("cli.help.save.keep") + ) + save_parser.add_argument("--password", default="", help=t("cli.help.save.password")) + + watch_parser = subparsers.add_parser("watch", help=t("cli.help.watch")) + watch_parser.add_argument( + "--interval", type=float, default=60, help=t("cli.help.watch.interval") + ) + watch_parser.add_argument( + "--keep", type=int, default=0, help=t("cli.help.watch.keep") + ) + watch_parser.add_argument( + "--password", default="", help=t("cli.help.watch.password") + ) + + restore_parser = subparsers.add_parser("restore", help=t("cli.help.restore")) + restore_parser.add_argument("backup_file", help=t("cli.help.restore.file")) + restore_parser.add_argument("target_dir", help=t("cli.help.restore.dir")) + + subparsers.add_parser("version", help=t("cli.help.version")) return parser def parse_path(path_str: str) -> str: - """ - 清理并解析路径 - """ return os.path.expanduser(path_str.strip()) def run() -> int: - """ - 主运行函数,返回退出码:0 成功,1 失败 - """ + # 先检测 --lang 参数,初始化语言环境,再创建本地化 parser + lang_from_argv = _detect_lang_from_argv() + config = load_config() + current_lang = lang_from_argv if lang_from_argv is not None else config.lang + set_locale(current_lang) + parser = get_parser() args = parser.parse_args() - # 加载配置以获取默认语言设置 - config = load_config() + # 持久化语言设置 + if lang_from_argv is not None: + save_lang(lang_from_argv) - # 检查是否显式提供了 --lang 参数 - if args.lang is not None: - current_lang = args.lang - # 持久化语言设置 - save_lang(current_lang) - else: - current_lang = config.lang + # 持久化格式设置 + if args.format is not None: + save_format(args.format) + config.compression_format = args.format.upper().replace(".", "_") - # 初始化语言环境 - set_locale(current_lang) - - # 配置日志:仅在有 --debug 参数时开启 if args.debug: logging.basicConfig( level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(message)s", - datefmt="%H:%M:%S" + datefmt="%H:%M:%S", ) if args.command is None: @@ -115,10 +147,7 @@ def run() -> int: return 0 if args.command == "version": - print(f""" -Sbackup v{VERSION} — Copyright © 2026 xiatianxuan -Licensed under GNU GPL v3.0 — https://www.gnu.org/licenses/gpl-3.0.html -""") + print(t("cli.version", version=VERSION)) return 0 manager = BackupManager(data_file=config.data_file) @@ -126,7 +155,9 @@ Licensed under GNU GPL v3.0 — https://www.gnu.org/licenses/gpl-3.0.html if args.command == "add": source = parse_path(args.source) dest = parse_path(args.dest) - success = manager.add_folder(source, dest, args.ignore) + # 条目级格式:用户在 add 时指定的 --format 仅作用于该条目 + entry_fmt = args.format.upper().replace(".", "_") if args.format else "" + success = manager.add_folder(source, dest, args.ignore, entry_fmt) if success: print(t("cmd.add.success", source=source, dest=dest)) return 0 @@ -142,5 +173,21 @@ Licensed under GNU GPL v3.0 — https://www.gnu.org/licenses/gpl-3.0.html print(manager.list_folder_table()) return 0 elif args.command == "save": - manager.execute_backups() + manager.execute_backups(keep=args.keep, password=args.password) return 0 + elif args.command == "watch": + import time as _time + + interval_sec = args.interval * 60 + print(t("cmd.watch.start", interval=args.interval)) + try: + while True: + manager.execute_backups(keep=args.keep, password=args.password) + _time.sleep(interval_sec) + except KeyboardInterrupt: + return 0 + elif args.command == "restore": + result = restore_backup(args.backup_file, args.target_dir) + return 0 if result["success"] else 1 + + return 0 diff --git a/sbackup/auto_save.py b/sbackup/auto_save.py index 2eee2e6..bd8ebdb 100644 --- a/sbackup/auto_save.py +++ b/sbackup/auto_save.py @@ -2,9 +2,15 @@ import os import json import shutil import logging +from pathlib import Path from dataclasses import dataclass -from sbackup.config import Config, load_config, get_default_data_file, DEFAULT_SKIP_PATTERNS -from sbackup.compression import ZipfileCompression +from sbackup.config import ( + Config, + load_config, + get_default_data_file, + DEFAULT_SKIP_PATTERNS, +) +from sbackup.compression import create_compressor from sbackup.i18n import t logger = logging.getLogger(__name__) @@ -13,24 +19,30 @@ logger = logging.getLogger(__name__) @dataclass class BackupEntry: """备份策略条目""" + mtime: float target: str skip_patterns: list[str] + compression_format: str = "" # 空字符串表示使用全局默认格式 def to_list(self) -> list: """转为 JSON 兼容的列表格式""" - return [self.mtime, self.target, self.skip_patterns] + return [self.mtime, self.target, self.skip_patterns, self.compression_format] @staticmethod def from_list(data: list) -> "BackupEntry": - """从 JSON 兼容的列表格式创建""" - return BackupEntry(mtime=data[0], target=data[1], skip_patterns=data[2]) + """从 JSON 兼容的列表格式创建(向后兼容旧格式)""" + fmt = data[3] if len(data) > 3 else "" + return BackupEntry( + mtime=data[0], target=data[1], skip_patterns=data[2], compression_format=fmt + ) class BackupManager: """ 管理备份策略的类,封装状态和读写操作 """ + def __init__(self, data_file: str = ""): self.data_file: str = data_file or get_default_data_file() self.data: dict[str, list] = {} @@ -40,12 +52,12 @@ class BackupManager: """ 从 JSON 文件加载数据到内存 """ - logger.debug(f"读取数据文件: {self.data_file}") + logger.debug(t("log.data.read"), self.data_file) if not os.path.exists(self.data_file): - logger.debug(f"数据文件不存在,创建新文件: {self.data_file}") + logger.debug(t("log.data.create"), self.data_file) self.save(initial=True) else: - logger.debug(f"加载现有数据文件: {self.data_file}") + logger.debug(t("log.data.load"), self.data_file) try: with open(self.data_file, "r", encoding="utf-8") as f: self.data = json.load(f) @@ -60,7 +72,9 @@ class BackupManager: # 备份失败时重命名损坏文件,避免下次再次触发 try: os.rename(self.data_file, self.data_file + ".corrupted") - print(t("warn.json.renamed", path=self.data_file + ".corrupted")) + print( + t("warn.json.renamed", path=self.data_file + ".corrupted") + ) except OSError: pass self.data = {} @@ -70,7 +84,7 @@ class BackupManager: 将内存数据写入 JSON 文件 """ if not initial: - logger.debug(f"写入数据文件: {self.data_file}") + logger.debug(t("log.data.write"), self.data_file) data_dir = os.path.dirname(self.data_file) if data_dir: @@ -95,13 +109,19 @@ class BackupManager: folder_path: str, target_folder: str, skip_patterns: str | None = None, + compression_format: str = "", ): """ 添加备份策略 + :param compression_format: 条目级打包格式,空字符串使用全局默认 """ if skip_patterns is None: skip_patterns = ",".join(DEFAULT_SKIP_PATTERNS) - skip_list = [s.strip() for s in skip_patterns.split(",") if s.strip()] if skip_patterns else [] + skip_list = ( + [s.strip() for s in skip_patterns.split(",") if s.strip()] + if skip_patterns + else [] + ) if not os.path.isdir(folder_path): print(t("err.folder.invalid", path=folder_path)) @@ -120,6 +140,7 @@ class BackupManager: mtime=os.stat(abs_path).st_mtime, target=os.path.abspath(target_folder), skip_patterns=skip_list, + compression_format=compression_format, ) except OSError as e: print(t("err.os", error=e)) @@ -141,14 +162,18 @@ class BackupManager: print(t("warn.no.strategy.found", path=abs_path)) return False - def execute_backups(self): + def execute_backups(self, keep: int = 0, password: str = ""): """ 执行所有备份策略 + :param keep: 保留最近 N 个备份文件,0 表示不清理 + :param password: 加密密码(仅 7z 格式支持) """ config = load_config() backup_count = 0 skip_count = 0 for key, raw in list(self.data.items()): + if key == "_history": + continue if not os.path.exists(key): print(t("warn.source.missing", path=key)) continue @@ -159,17 +184,24 @@ class BackupManager: continue entry = BackupEntry.from_list(raw) if entry.mtime != current_mtime: + # 条目级格式优先,否则使用全局配置 + fmt = entry.compression_format or config.compression_format config_instance = Config( folder_path=key, zipfile_path=entry.target, skip_patterns=entry.skip_patterns, + compression_format=fmt, compression_algorithm=config.compression_algorithm, - compression_level=config.compression_level + compression_level=config.compression_level, + password=password, ) - result = ZipfileCompression(config_instance).zip_folder() + result = create_compressor(config_instance).compress() if result["success"]: entry.mtime = current_mtime self._set_entry(key, entry) + self._add_history(key, result["size_mb"], result["files_count"]) + if keep > 0: + self._cleanup_old_backups(entry.target, keep) backup_count += 1 else: skip_count += 1 @@ -182,15 +214,73 @@ class BackupManager: # 向后兼容别名 save_folder = execute_backups + def _add_history(self, source: str, size_mb: float, files_count: int): + """记录备份历史""" + from datetime import datetime + + history = self.data.setdefault("_history", []) + history.append( + { + "time": datetime.now().isoformat(timespec="seconds"), + "source": source, + "size_mb": round(size_mb, 2), + "files_count": files_count, + } + ) + # 保留最近 100 条记录 + if len(history) > 100: + self.data["_history"] = history[-100:] + + def get_history(self) -> list[dict]: + """获取备份历史记录""" + return self.data.get("_history", []) + + @staticmethod + def _cleanup_old_backups(target_dir: str, keep: int): + """清理旧备份文件,仅保留最近 keep 个(keep=0 时不清理)""" + if keep <= 0: + return + + target = Path(target_dir) + if not target.is_dir(): + return + # 收集所有备份文件(.zip / .tar.* / .7z) + patterns = [ + "*.zip", + "*.tar", + "*.tar.gz", + "*.tar.bz2", + "*.tar.xz", + "*.tar.zst", + "*.7z", + ] + files = [] + for pat in patterns: + files.extend(target.glob(pat)) + if len(files) <= keep: + return + # 按修改时间排序,删除旧的 + files.sort(key=lambda f: f.stat().st_mtime, reverse=True) + for old_file in files[keep:]: + try: + old_file.unlink() + logger.debug(t("log.cleanup.delete"), old_file) + except OSError: + pass + def all_folder(self) -> dict[str, str]: """ 查看所有备份策略 """ - return {key: BackupEntry.from_list(raw).target for key, raw in self.data.items()} + return { + key: BackupEntry.from_list(raw).target for key, raw in self.data.items() + } @staticmethod def _display_width(s: str) -> int: """计算字符串的终端显示宽度(中文字符算2,英文字符算1)""" + if not isinstance(s, str): + return len(str(s)) width = 0 for ch in s: if ord(ch) > 0x2E80: @@ -206,12 +296,28 @@ class BackupManager: if not self.data: return t("cmd.all.empty") - headers = [t("table.header.source"), t("table.header.dest"), t("table.header.ignore")] + headers = [ + t("table.header.source"), + t("table.header.dest"), + t("table.header.format"), + t("table.header.ignore"), + ] rows = [] for path, raw in self.data.items(): + if path == "_history": + continue entry = BackupEntry.from_list(raw) - skip = ", ".join(entry.skip_patterns) if entry.skip_patterns else t("table.cell.none") - rows.append([path, entry.target, skip]) + fmt_display = ( + entry.compression_format + if entry.compression_format + else t("table.cell.default") + ) + skip = ( + ", ".join(entry.skip_patterns) + if entry.skip_patterns + else t("table.cell.none") + ) + rows.append([path, entry.target, fmt_display, skip]) col_widths = [self._display_width(h) for h in headers] for row in rows: @@ -228,4 +334,3 @@ class BackupManager: lines.append(fmt.format(*row)) return "\n".join(lines) - diff --git a/sbackup/compression.py b/sbackup/compression.py index f215a22..17e7259 100644 --- a/sbackup/compression.py +++ b/sbackup/compression.py @@ -1,8 +1,10 @@ """ -压缩模块:ZIP 文件压缩逻辑 +压缩模块:ZIP / TAR / Zstd / 7z 文件压缩逻辑 """ import os +import io +import tarfile import zipfile from pathlib import Path from fnmatch import fnmatch @@ -13,14 +15,84 @@ from sbackup.config import Config # compresslevel 仅对 ZIP_DEFLATED 和 ZIP_BZIP2 有效 _VALID_COMPRESSLEVEL_ALGORITHMS = {zipfile.ZIP_DEFLATED, zipfile.ZIP_BZIP2} +# tar 格式 → (扩展名, 打开模式) +_TAR_FORMATS = { + "TAR": (".tar", "w"), + "TAR_GZ": (".tar.gz", "w:gz"), + "TAR_BZ2": (".tar.bz2", "w:bz2"), + "TAR_XZ": (".tar.xz", "w:xz"), +} + + +class BaseCompressor: + """压缩器基类,提供公共的文件收集和忽略逻辑""" -class ZipfileCompression: def __init__(self, config: Config) -> None: self.folder_path: Path = Path(config.folder_path) - self.zipfile_path: Path | None = Path(config.zipfile_path) if config.zipfile_path else None + self.zipfile_path: Path | None = ( + Path(config.zipfile_path) if config.zipfile_path else None + ) self.skip_patterns: list[str] = config.skip_patterns - self.compression_algorithm: int = self._choose_compression_algorithm(config.compression_algorithm) - self.compression_level: int | None = self._validate_compresslevel( + self.compression_level: int | None = None + + def _should_ignore(self, rel_path: str) -> bool: + """检查相对路径是否匹配忽略模式(支持路径级匹配如 subdir/*.log)""" + for pattern in self.skip_patterns: + if fnmatch(rel_path, pattern) or fnmatch( + os.path.basename(rel_path), pattern + ): + return True + return False + + def _collect_files(self, folder_path: Path) -> list[tuple[str, str]]: + """遍历文件夹收集需要压缩的文件列表,处理权限错误""" + files = [] + try: + for dirpath, dirnames, filenames in os.walk(folder_path): + try: + rel_dir = os.path.relpath(dirpath, folder_path) + if rel_dir == ".": + rel_dir = "" + dirnames[:] = [ + d + for d in dirnames + if not self._should_ignore( + os.path.join(rel_dir, d).replace("\\", "/") + if rel_dir + else d + ) + ] + for filename in filenames: + file_rel = ( + os.path.join(rel_dir, filename).replace("\\", "/") + if rel_dir + else filename + ) + if not self._should_ignore(file_rel): + files.append((dirpath, filename)) + except PermissionError: + print(t("err.permission", path=dirpath)) + continue + except PermissionError: + print(t("err.permission", path=folder_path)) + except OSError as e: + print(t("err.os", error=e)) + return files + + def compress(self) -> dict: + """子类实现具体压缩逻辑""" + raise NotImplementedError + + +class ZipfileCompression(BaseCompressor): + """ZIP 格式压缩器""" + + def __init__(self, config: Config) -> None: + super().__init__(config) + self.compression_algorithm: int = self._choose_compression_algorithm( + config.compression_algorithm + ) + self.compression_level = self._validate_compresslevel( config.compression_level, self.compression_algorithm ) @@ -40,63 +112,33 @@ class ZipfileCompression: @staticmethod def _validate_compresslevel(level: int, algorithm: int) -> int | None: - """校验 compresslevel:仅对 ZIP_DEFLATED 和 ZIP_BZIP2 有效,其他算法不传递该参数""" if algorithm not in _VALID_COMPRESSLEVEL_ALGORITHMS: - return None # zipfile 不接受 compresslevel 时传 None + return None if not (0 <= level <= 9): print(t("warn.invalid.compresslevel", level=level)) return 6 return level - def _should_ignore(self, name: str) -> bool: - for pattern in self.skip_patterns: - if fnmatch(name, pattern): - return True - return False + def _resolve_zipfile_path(self, folder_path: Path) -> Path: + if self.zipfile_path is None: + return folder_path.parent / f"{folder_path.name}.zip" + zipfile_path = self.zipfile_path.resolve() + if zipfile_path.is_dir(): + return zipfile_path / f"{folder_path.name}.zip" + if zipfile_path.suffix.lower() != ".zip": + return zipfile_path.with_name(zipfile_path.name + ".zip") + return zipfile_path - def _collect_files(self, folder_path: Path) -> list[tuple[str, str]]: - """遍历文件夹收集需要压缩的文件列表,处理权限错误""" - files = [] - try: - for dirpath, dirnames, filenames in os.walk(folder_path): - try: - dirnames[:] = [d for d in dirnames if not self._should_ignore(d)] - for filename in filenames: - if not self._should_ignore(filename): - files.append((dirpath, filename)) - except PermissionError as e: - print(t("err.permission", path=dirpath)) - continue - except PermissionError as e: - print(t("err.permission", path=folder_path)) - except OSError as e: - print(t("err.os", error=e)) - return files - - def zip_folder(self) -> dict: - """ - 压缩指定文件夹到 ZIP 文件 - :return: 包含统计信息的字典 - """ + def compress(self) -> dict: folder_path = self.folder_path.resolve() if not folder_path.is_dir(): print(t("err.folder.invalid", path=folder_path)) return {"success": False, "files_count": 0, "size_mb": 0.0} - if self.zipfile_path is None: - zipfile_path = folder_path.parent / f"{folder_path.name}.zip" - else: - zipfile_path = self.zipfile_path.resolve() - if zipfile_path.is_dir(): - zipfile_path = zipfile_path / f"{folder_path.name}.zip" - elif zipfile_path.suffix.lower() != ".zip": - zipfile_path = zipfile_path.with_name(zipfile_path.name + ".zip") - - # ZIP 文件已存在时提示覆盖 + zipfile_path = self._resolve_zipfile_path(folder_path) if zipfile_path.exists(): print(t("warn.zip.overwrite", path=zipfile_path)) - # 1. 收集需要压缩的文件(带异常保护) files_to_compress = self._collect_files(folder_path) total_files = len(files_to_compress) files_count = 0 @@ -107,22 +149,32 @@ class ZipfileCompression: zip_kwargs["compresslevel"] = self.compression_level with zipfile.ZipFile(zipfile_path, **zip_kwargs) as zipf: - # 2. 使用 tqdm 显示进度条 - with tqdm(total=total_files, desc=t("compress.progress"), unit=t("compress.unit")) as pbar: + with tqdm( + total=total_files, + desc=t("compress.progress"), + unit=t("compress.unit"), + ) as pbar: for dirpath, filename in files_to_compress: file_path = Path(dirpath) / filename - arcname = str(folder_path.name / file_path.relative_to(folder_path)).replace("\\", "/") + arcname = str( + folder_path.name / file_path.relative_to(folder_path) + ).replace("\\", "/") try: zipf.write(file_path, arcname) pbar.update(1) files_count += 1 except (FileNotFoundError, PermissionError): - # 文件在遍历后被删除或权限变更,跳过单个文件 continue size_mb = zipfile_path.stat().st_size / (1024 * 1024) - # tqdm 清理输出后打印最终结果 - print(t("compress.success", path=zipfile_path, size=size_mb, count=files_count)) + print( + t( + "compress.success", + path=zipfile_path, + size=size_mb, + count=files_count, + ) + ) return {"success": True, "files_count": files_count, "size_mb": size_mb} except KeyboardInterrupt: raise @@ -134,4 +186,370 @@ class ZipfileCompression: return {"success": False, "files_count": 0, "size_mb": 0.0} except Exception as e: print(t("err.unknown", error=e)) - return {"success": False, "files_count": 0, "size_mb": 0.0} \ No newline at end of file + return {"success": False, "files_count": 0, "size_mb": 0.0} + + +class TarfileCompression(BaseCompressor): + """TAR 格式压缩器(支持 tar.gz / tar.bz2 / tar.xz)""" + + def __init__(self, config: Config) -> None: + super().__init__(config) + fmt = config.compression_format.upper() + if fmt not in _TAR_FORMATS: + fmt = "TAR_GZ" + self._extension, self._mode = _TAR_FORMATS[fmt] + self.compression_level = self._validate_compresslevel(config.compression_level) + + @staticmethod + def _validate_compresslevel(level: int) -> int: + if not (0 <= level <= 9): + print(t("warn.invalid.compresslevel", level=level)) + return 6 + return level + + def _resolve_tarfile_path(self, folder_path: Path) -> Path: + if self.zipfile_path is None: + return folder_path.parent / f"{folder_path.name}{self._extension}" + tarfile_path = self.zipfile_path.resolve() + if tarfile_path.is_dir(): + return tarfile_path / f"{folder_path.name}{self._extension}" + # 如果已有后缀,直接使用;否则追加 + name = tarfile_path.name + if not any( + name.endswith(ext) for ext in [".tar.gz", ".tar.bz2", ".tar.xz", ".tar"] + ): + return tarfile_path.with_name(name + self._extension) + return tarfile_path + + def compress(self) -> dict: + folder_path = self.folder_path.resolve() + if not folder_path.is_dir(): + print(t("err.folder.invalid", path=folder_path)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + + tarfile_path = self._resolve_tarfile_path(folder_path) + if tarfile_path.exists(): + print(t("warn.zip.overwrite", path=tarfile_path)) + + files_to_compress = self._collect_files(folder_path) + total_files = len(files_to_compress) + files_count = 0 + + try: + # compresslevel 仅对 gz 和 bz2 模式有效 + tar_kwargs = {"name": tarfile_path, "mode": self._mode} + if self._mode in ("w:gz", "w:bz2"): + tar_kwargs["compresslevel"] = self.compression_level + + with tarfile.open(**tar_kwargs) as tarf: + with tqdm( + total=total_files, + desc=t("compress.progress"), + unit=t("compress.unit"), + ) as pbar: + for dirpath, filename in files_to_compress: + file_path = Path(dirpath) / filename + arcname = str( + folder_path.name / file_path.relative_to(folder_path) + ).replace("\\", "/") + try: + tarf.add(file_path, arcname=arcname, recursive=False) + pbar.update(1) + files_count += 1 + except (FileNotFoundError, PermissionError): + continue + + size_mb = tarfile_path.stat().st_size / (1024 * 1024) + print( + t( + "compress.success", + path=tarfile_path, + size=size_mb, + count=files_count, + ) + ) + return {"success": True, "files_count": files_count, "size_mb": size_mb} + except KeyboardInterrupt: + raise + except PermissionError: + print(t("err.permission", path=tarfile_path)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + except OSError as e: + print(t("err.os", error=e)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + except Exception as e: + print(t("err.unknown", error=e)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + + +class ZstdCompression(BaseCompressor): + """tar.zst 格式压缩器(使用 zstandard 库)""" + + def __init__(self, config: Config) -> None: + super().__init__(config) + self.compression_level = self._validate_compresslevel(config.compression_level) + + @staticmethod + def _validate_compresslevel(level: int) -> int: + if not (0 <= level <= 22): + print(t("warn.invalid.compresslevel", level=level)) + return 3 + return level + + def _resolve_path(self, folder_path: Path) -> Path: + if self.zipfile_path is None: + return folder_path.parent / f"{folder_path.name}.tar.zst" + path = self.zipfile_path.resolve() + if path.is_dir(): + return path / f"{folder_path.name}.tar.zst" + if not path.name.endswith(".tar.zst"): + return path.with_name(path.name + ".tar.zst") + return path + + def compress(self) -> dict: + import zstandard as zstd + + folder_path = self.folder_path.resolve() + if not folder_path.is_dir(): + print(t("err.folder.invalid", path=folder_path)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + + output_path = self._resolve_path(folder_path) + if output_path.exists(): + print(t("warn.zip.overwrite", path=output_path)) + + files_to_compress = self._collect_files(folder_path) + total_files = len(files_to_compress) + files_count = 0 + + try: + cctx = zstd.ZstdCompressor(level=self.compression_level) + # 先创建 tar,再用 zstd 压缩 + tar_buffer = io.BytesIO() + with tarfile.open(fileobj=tar_buffer, mode="w") as tarf: + with tqdm( + total=total_files, + desc=t("compress.progress"), + unit=t("compress.unit"), + ) as pbar: + for dirpath, filename in files_to_compress: + file_path = Path(dirpath) / filename + arcname = str( + folder_path.name / file_path.relative_to(folder_path) + ).replace("\\", "/") + try: + tarf.add(file_path, arcname=arcname, recursive=False) + pbar.update(1) + files_count += 1 + except (FileNotFoundError, PermissionError): + continue + + compressed = cctx.compress(tar_buffer.getvalue()) + output_path.write_bytes(compressed) + + size_mb = output_path.stat().st_size / (1024 * 1024) + print( + t("compress.success", path=output_path, size=size_mb, count=files_count) + ) + return {"success": True, "files_count": files_count, "size_mb": size_mb} + except KeyboardInterrupt: + raise + except PermissionError: + print(t("err.permission", path=output_path)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + except OSError as e: + print(t("err.os", error=e)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + except Exception as e: + print(t("err.unknown", error=e)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + + +class SevenZipCompression(BaseCompressor): + """7z 格式压缩器(使用 py7zr 库)""" + + def __init__(self, config: Config) -> None: + super().__init__(config) + self.compression_level = self._validate_compresslevel(config.compression_level) + self.password = config.password if config.password else None + + @staticmethod + def _validate_compresslevel(level: int) -> int: + if not (0 <= level <= 9): + print(t("warn.invalid.compresslevel", level=level)) + return 6 + return level + + def _resolve_path(self, folder_path: Path) -> Path: + if self.zipfile_path is None: + return folder_path.parent / f"{folder_path.name}.7z" + path = self.zipfile_path.resolve() + if path.is_dir(): + return path / f"{folder_path.name}.7z" + if path.suffix.lower() != ".7z": + return path.with_name(path.name + ".7z") + return path + + def compress(self) -> dict: + import py7zr + + folder_path = self.folder_path.resolve() + if not folder_path.is_dir(): + print(t("err.folder.invalid", path=folder_path)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + + output_path = self._resolve_path(folder_path) + if output_path.exists(): + print(t("warn.zip.overwrite", path=output_path)) + + files_to_compress = self._collect_files(folder_path) + total_files = len(files_to_compress) + files_count = 0 + + try: + filters = [{"id": py7zr.FILTER_LZMA2, "preset": self.compression_level}] + szf_kwargs = {"mode": "w", "filters": filters} + if self.password: + szf_kwargs["password"] = self.password + with py7zr.SevenZipFile(output_path, **szf_kwargs) as szf: + with tqdm( + total=total_files, + desc=t("compress.progress"), + unit=t("compress.unit"), + ) as pbar: + for dirpath, filename in files_to_compress: + file_path = Path(dirpath) / filename + arcname = str( + folder_path.name / file_path.relative_to(folder_path) + ).replace("\\", "/") + try: + szf.write(file_path, arcname) + pbar.update(1) + files_count += 1 + except (FileNotFoundError, PermissionError): + continue + + size_mb = output_path.stat().st_size / (1024 * 1024) + print( + t("compress.success", path=output_path, size=size_mb, count=files_count) + ) + return {"success": True, "files_count": files_count, "size_mb": size_mb} + except KeyboardInterrupt: + raise + except PermissionError: + print(t("err.permission", path=output_path)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + except OSError as e: + print(t("err.os", error=e)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + except Exception as e: + print(t("err.unknown", error=e)) + return {"success": False, "files_count": 0, "size_mb": 0.0} + + +def create_compressor(config: Config) -> BaseCompressor: + """工厂函数:根据配置创建对应的压缩器""" + fmt = config.compression_format.upper() + if fmt in _TAR_FORMATS: + return TarfileCompression(config) + if fmt == "TAR_ZST": + return ZstdCompression(config) + if fmt == "7Z": + return SevenZipCompression(config) + return ZipfileCompression(config) + + +def restore_backup(backup_path: str, target_dir: str) -> dict: + """ + 从备份文件还原到目标目录 + 自动检测格式(ZIP / tar.gz / tar.bz2 / tar.xz) + :return: 包含统计信息的字典 + """ + backup = Path(backup_path) + if not backup.exists(): + print(t("err.folder.invalid", path=backup_path)) + return {"success": False, "files_count": 0} + + target = Path(target_dir) + target.mkdir(parents=True, exist_ok=True) + + name_lower = backup.name.lower() + try: + if name_lower.endswith(".zip"): + with zipfile.ZipFile(backup, "r") as zf: + members = zf.namelist() + with tqdm( + total=len(members), + desc=t("restore.progress"), + unit=t("compress.unit"), + ) as pbar: + for member in members: + zf.extract(member, target) + pbar.update(1) + print(t("restore.success", path=target, count=len(members))) + return {"success": True, "files_count": len(members)} + elif name_lower.endswith(".7z"): + import py7zr + + with py7zr.SevenZipFile(backup, "r") as szf: + members = szf.getnames() + with tqdm( + total=len(members), + desc=t("restore.progress"), + unit=t("compress.unit"), + ) as pbar: + szf.extractall(target) + pbar.update(len(members)) + print(t("restore.success", path=target, count=len(members))) + return {"success": True, "files_count": len(members)} + elif name_lower.endswith(".tar.zst"): + import zstandard as zstd + + dctx = zstd.ZstdDecompressor() + compressed = backup.read_bytes() + tar_data = dctx.decompress(compressed) + with tarfile.open(fileobj=io.BytesIO(tar_data), mode="r") as tarf: + members = tarf.getmembers() + with tqdm( + total=len(members), + desc=t("restore.progress"), + unit=t("compress.unit"), + ) as pbar: + for member in members: + tarf.extract(member, target, filter="data") + pbar.update(1) + print(t("restore.success", path=target, count=len(members))) + return {"success": True, "files_count": len(members)} + elif name_lower.endswith(".tar.gz") or name_lower.endswith(".tgz"): + mode = "r:gz" + elif name_lower.endswith(".tar.bz2") or name_lower.endswith(".tbz2"): + mode = "r:bz2" + elif name_lower.endswith(".tar.xz") or name_lower.endswith(".txz"): + mode = "r:xz" + elif name_lower.endswith(".tar"): + mode = "r" + else: + print(t("err.unknown.format", path=backup_path)) + return {"success": False, "files_count": 0} + + with tarfile.open(backup, mode) as tarf: + members = tarf.getmembers() + with tqdm( + total=len(members), desc=t("restore.progress"), unit=t("compress.unit") + ) as pbar: + for member in members: + tarf.extract(member, target, filter="data") + pbar.update(1) + print(t("restore.success", path=target, count=len(members))) + return {"success": True, "files_count": len(members)} + except KeyboardInterrupt: + raise + except PermissionError: + print(t("err.permission", path=target_dir)) + return {"success": False, "files_count": 0} + except OSError as e: + print(t("err.os", error=e)) + return {"success": False, "files_count": 0} + except Exception as e: + print(t("err.unknown", error=e)) + return {"success": False, "files_count": 0} diff --git a/sbackup/config.py b/sbackup/config.py index fb06d82..6a0e22e 100644 --- a/sbackup/config.py +++ b/sbackup/config.py @@ -7,6 +7,7 @@ import sys import json import logging from dataclasses import dataclass, field +from sbackup.i18n import t logger = logging.getLogger(__name__) @@ -28,12 +29,15 @@ def get_default_data_file() -> str: class Config: folder_path: str = "." zipfile_path: str | None = None - skip_patterns: list[str] = field(default_factory=lambda: list(DEFAULT_SKIP_PATTERNS)) + skip_patterns: list[str] = field( + default_factory=lambda: list(DEFAULT_SKIP_PATTERNS) + ) compression_format: str = "ZIP" compression_algorithm: str = "ZIP_DEFLATED" compression_level: int = 6 - lang: str = "en_US" + lang: str = "zh_CN" data_file: str = field(default_factory=get_default_data_file) + password: str = "" def load_config(config_file: str = "config.json") -> Config: @@ -47,23 +51,24 @@ def load_config(config_file: str = "config.json") -> Config: with open(config_file, "r", encoding="utf-8") as f: config_data = json.load(f) except json.JSONDecodeError: - logger.warning("配置文件 %s 格式错误,使用默认配置", config_file) + logger.warning(t("log.config.malformed"), config_file) return Config() compression_config = config_data.get("compression", {}) skip_patterns = config_data.get("skip_patterns", DEFAULT_SKIP_PATTERNS) data_file = config_data.get("data_file", get_default_data_file()) lang = config_data.get("lang", "en_US") + compression_format = config_data.get("compression_format", "ZIP") return Config( folder_path="", zipfile_path=None, skip_patterns=skip_patterns, - compression_format="ZIP", + compression_format=compression_format, compression_algorithm=compression_config.get("algorithm", "ZIP_DEFLATED"), compression_level=compression_config.get("level", 6), lang=lang, - data_file=data_file + data_file=data_file, ) @@ -76,7 +81,7 @@ def save_lang(lang: str, config_file: str = "config.json") -> None: with open(config_file, "r", encoding="utf-8") as f: data = json.load(f) except json.JSONDecodeError: - logger.warning("配置文件 %s 格式错误,重置为空", config_file) + logger.warning(t("log.config.reset"), config_file) data = {} else: data = {} @@ -88,11 +93,42 @@ def save_lang(lang: str, config_file: str = "config.json") -> None: try: os.makedirs(data_dir, exist_ok=True) except OSError as e: - logger.error("无法创建配置目录 %s: %s", data_dir, e) + logger.error(t("log.config.mkdir.error"), data_dir, e) return try: with open(config_file, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=4) except OSError as e: - logger.error("无法写入配置文件 %s: %s", config_file, e) \ No newline at end of file + logger.error(t("log.config.write.error"), config_file, e) + + +def save_format(fmt: str, config_file: str = "config.json") -> None: + """ + 将打包格式偏好保存到配置文件 + """ + if os.path.exists(config_file): + try: + with open(config_file, "r", encoding="utf-8") as f: + data = json.load(f) + except json.JSONDecodeError: + logger.warning(t("log.config.reset"), config_file) + data = {} + else: + data = {} + + data["compression_format"] = fmt + + data_dir = os.path.dirname(config_file) + if data_dir: + try: + os.makedirs(data_dir, exist_ok=True) + except OSError as e: + logger.error(t("log.config.mkdir.error"), data_dir, e) + return + + try: + with open(config_file, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + except OSError as e: + logger.error(t("log.config.write.error"), config_file, e) diff --git a/sbackup/i18n.py b/sbackup/i18n.py index a817548..dddb455 100644 --- a/sbackup/i18n.py +++ b/sbackup/i18n.py @@ -14,7 +14,8 @@ if os.path.exists(_default_file): with open(_default_file, "r", encoding="utf-8") as f: _translations = json.load(f) except (json.JSONDecodeError, OSError) as e: - logger.warning("无法加载默认语言包 %s: %s", _default_file, e) + logger.warning("i18n.load.default_failed %s: %s", _default_file, e) + def set_locale(lang: str) -> None: """ @@ -29,7 +30,7 @@ def set_locale(lang: str) -> None: _translations = json.load(f) return except (json.JSONDecodeError, OSError) as e: - logger.warning("无法加载语言包 %s: %s", locale_file, e) + logger.warning(t("log.i18n.load.failed", path=locale_file, error=e)) # 回退到默认中文 default_file = os.path.join(os.path.dirname(__file__), "locales", "zh_CN.json") @@ -38,9 +39,12 @@ def set_locale(lang: str) -> None: with open(default_file, "r", encoding="utf-8") as f: _translations = json.load(f) except (json.JSONDecodeError, OSError) as e: - logger.warning("无法加载回退语言包 %s: %s", default_file, e) + logger.warning( + t("log.i18n.load.fallback_failed", path=default_file, error=e) + ) _translations = {} + def t(key: str, **kwargs) -> str: """ 获取翻译文本 @@ -54,4 +58,4 @@ def t(key: str, **kwargs) -> str: return text.format(**kwargs) except (KeyError, ValueError): return text - return text \ No newline at end of file + return text diff --git a/sbackup/locales/de_DE.json b/sbackup/locales/de_DE.json new file mode 100644 index 0000000..36c8efb --- /dev/null +++ b/sbackup/locales/de_DE.json @@ -0,0 +1,80 @@ +{ + "cmd.add.success": "Backup-Strategie erfolgreich hinzugefügt: {source} -> {dest}", + "cmd.rm.success": "Backup-Strategie erfolgreich entfernt: {path}", + "err.folder.invalid": "Fehler: {path} ist kein gültiger Ordner oder existiert nicht.", + "err.dest.invalid": "Fehler: Zielordner {path} ist kein gültiger Ordner oder existiert nicht.", + "info.already.added": "Info: {path} wurde bereits hinzugefügt. Bitte nicht duplizieren.", + "warn.no.strategy.found": "Warnung: Backup-Strategie für {path} nicht gefunden.", + "warn.source.missing": "Warnung: Quellordner existiert nicht: {path}", + "warn.json.decode.error": "Warnung: Datendatei {path} ist fehlerhaft, Initialisierung als leeres Dictionary.", + "warn.json.backup": "Warnung: Fehlerhafte Datendatei wurde nach {path} kopiert.", + "warn.json.renamed": "Warnung: Fehlerhafte Datendatei wurde nach {path} umbenannt.", + "compress.success": "Backup erfolgreich: {path} ({size:.2f} MB, {count} Dateien)", + "compress.progress": "Komprimierung", + "compress.unit": "Dateien", + "err.permission": "Zugriff verweigert: Kann nicht in '{path}' schreiben", + "err.os": "Systemfehler: {error}", + "err.unknown": "Unbekannter Fehler: {error}", + "err.unknown.format": "Fehler: Unbekanntes Backup-Dateiformat {path}", + "err.argparse.invalid_choice": "ungültige Auswahl: ", + "err.argparse.choose_from": "auswählen aus", + "err.argparse.invalid_float": "ungültiger Float-Wert: ", + "err.argparse.invalid_int": "ungültiger Int-Wert: ", + "err.argparse.unrecognized_args": "unbekannte Argumente: ", + "err.argparse.required": "erforderlich", + "warn.zip.overwrite": "Warnung: Überschreiben der vorhandenen Backup-Datei {path}", + "warn.invalid.compresslevel": "Warnung: Komprimierungsstufe {level} außerhalb des Bereichs (0-9), Standardwert 6 wird verwendet", + "cmd.save.uptodate": "Alle Backup-Strategien sind aktuell, kein Backup erforderlich.", + "cmd.save.completed": "Backup abgeschlossen, {count} Strategie(n) bearbeitet.", + "cmd.all.empty": "Keine Backup-Strategien konfiguriert.", + "exit.message": "\nBeendet.", + "table.header.source": "Quellordner", + "table.header.dest": "Zielordner", + "table.header.ignore": " Ausschlussmuster", + "table.cell.none": "Keine", + "table.header.format": "Format", + "table.cell.default": "Standard", + "restore.progress": "Wiederherstellung", + "restore.success": "Wiederherstellung erfolgreich: {path} ({count} Dateien)", + "cmd.watch.start": "Überwachung gestartet, Intervall {interval} Min. (Ctrl+C zum Stoppen)", + + "cli.description": "Sbackup v{version} — Leichtes inkrementelles Backup-Tool\n\nSbackup hilft Ihnen, Multi-Ordner-Backup-Aufgaben einfach zu verwalten.\nEs nutzt eine inkrementelle Backup-Strategie, komprimiert nur geänderte Dateien,\nunterstützt benutzerdefinierte Ausschlussregeln und bietet intuitives Fortschrittsfeedback.\n\nVerfügbare Befehle:\n add Neue Backup-Strategie hinzufügen (Quelle -> Ziel)\n rm Existing Backup-Strategie entfernen\n all Alle konfigurierten Backup-Strategien anzeigen\n save Backup-Aufgaben ausführen\n version Versionsinformationen anzeigen", + "cli.epilog": "Beispiele:\n Backup-Strategie hinzufügen:\n sbackup add F:/my_folder F:/backup -i node_modules,.git\n\n Strategie mit eintragsbezogenem Format hinzufügen:\n sbackup add F:/my_folder F:/backup --format tar.gz\n\n Backup-Strategie entfernen:\n sbackup rm F:/my_folder\n\n Alle Strategien anzeigen:\n sbackup all\n\n Backup ausführen (ZIP):\n sbackup save\n\n Backup ausführen (tar.gz):\n sbackup --format tar.gz save\n\n Backup ausführen (7z verschlüsselt):\n sbackup --format 7z save --password 123456\n\n Letzte 5 Backups behalten:\n sbackup save --keep 5\n\n Geplantes Backup (alle 60 Min.):\n sbackup watch --interval 60\n\n Überwachung + Bereinigung:\n sbackup watch --interval 120 --keep 10\n\n Backup wiederherstellen:\n sbackup restore backup.7z F:/restored\n\n Chinesische Oberfläche:\n sbackup --lang zh_CN save\n\n Debug-Modus:\n sbackup --debug save", + "cli.help.debug": "Debug-Modus mit detaillierter Protokollierung aktivieren", + "cli.help.help": "Diese Hilfe anzeigen und beenden", + "cli.help.lang": "Sprache der Oberfläche festlegen: zh_CN / en_US / fr_FR / es_ES / ru_RU / de_DE / ja_JP / pt_BR / ko_KR", + "cli.help.format": "Archivformat festlegen: zip (Standard) / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.subcommands": "Befehl zum Ausführen wählen", + "cli.help.add": "Neue Backup-Strategie hinzufügen", + "cli.help.add.source": "Quellordnerpfad: das zu sichernde Verzeichnis", + "cli.help.add.dest": "Zielordnerpfad: Speicherort für Backup-Dateien", + "cli.help.add.ignore": "Dateien oder Ordner ausschließen (komma-separiert, Standard: .git,__pycache__)", + "cli.help.add.format": "Eintragsbezogenes Archivformat (nur für diese Strategie, Standard global): zip / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.rm": "Backup-Strategie entfernen", + "cli.help.rm.path": "Quellordnerpfad der zu entferndenden Strategie", + "cli.help.all": "Alle Backup-Strategien anzeigen", + "cli.help.save": "Alle Backup-Strategien ausführen", + "cli.help.save.keep": "Die letzten N Backup-Dateien behalten, 0 deaktiviert Bereinigung", + "cli.help.save.password": "Verschlüsselungspasswort (nur für 7z-Format)", + "cli.help.watch": "Backups regelmäßig ausführen", + "cli.help.watch.interval": "Backup-Intervall in Minuten, Standard 60", + "cli.help.watch.keep": "Die letzten N Backup-Dateien behalten", + "cli.help.watch.password": "Verschlüsselungspasswort (nur für 7z-Format)", + "cli.help.restore": "Aus einer Backup-Datei wiederherstellen", + "cli.help.restore.file": "Backup-Dateipfad (.zip / .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .7z)", + "cli.help.restore.dir": "Zielverzeichnis für Wiederherstellung", + "cli.help.version": "Versionsinformationen anzeigen", + "cli.version": "\nSbackup v{version} — Copyright © 2026 xiatianxuan\nLizenz GNU GPL v3.0 — https://www.gnu.org/licenses/gpl-3.0.html\n", + + "log.config.malformed": "Konfigurationsdatei %s ist fehlerhaft, Standardwerte werden verwendet", + "log.config.reset": "Konfigurationsdatei %s ist fehlerhaft, Zurücksetzen", + "log.config.mkdir.error": "Konfigurationsverzeichnis %s kann nicht erstellt werden: %s", + "log.config.write.error": "Konfigurationsdatei %s kann nicht geschrieben werden: %s", + "log.data.read": "Datendatei lesen: %s", + "log.data.create": "Datendatei existiert nicht, Erstellen: %s", + "log.data.load": "Existing Datendatei laden: %s", + "log.data.write": "Datendatei schreiben: %s", + "log.cleanup.delete": "Altes Backup gelöscht: %s", + "log.i18n.load.failed": "Locale {path} konnte nicht geladen werden: {error}", + "log.i18n.load.fallback_failed": "Fallback-Locale {path} konnte nicht geladen werden: {error}" +} \ No newline at end of file diff --git a/sbackup/locales/en_US.json b/sbackup/locales/en_US.json index 4df4549..761bfc7 100644 --- a/sbackup/locales/en_US.json +++ b/sbackup/locales/en_US.json @@ -15,6 +15,13 @@ "err.permission": "Permission denied: Cannot write to '{path}'", "err.os": "OS Error: {error}", "err.unknown": "Unknown error: {error}", + "err.unknown.format": "Error: Unrecognized backup file format {path}", + "err.argparse.invalid_choice": "invalid choice: ", + "err.argparse.choose_from": "choose from", + "err.argparse.invalid_float": "invalid float value: ", + "err.argparse.invalid_int": "invalid int value: ", + "err.argparse.unrecognized_args": "unrecognized arguments: ", + "err.argparse.required": "required", "warn.zip.overwrite": "Warning: Overwriting existing backup file {path}", "warn.invalid.compresslevel": "Warning: Compression level {level} out of range (0-9), using default 6", "cmd.save.uptodate": "All backup strategies are up to date, no backup needed.", @@ -24,5 +31,50 @@ "table.header.source": "Source Folder", "table.header.dest": "Target Folder", "table.header.ignore": "Ignore Patterns", - "table.cell.none": "None" -} \ No newline at end of file + "table.cell.none": "None", + "table.header.format": "Format", + "table.cell.default": "Default", + "restore.progress": "Restoring", + "restore.success": "Restore successful: {path} ({count} files)", + "cmd.watch.start": "Watch started, interval {interval} min (press Ctrl+C to stop)", + + "cli.description": "Sbackup v{version} — Lightweight incremental backup tool\n\nSbackup helps you easily manage multi-folder backup tasks. It uses incremental backup strategy,\nonly compressing modified files, supporting custom ignore rules, and providing intuitive progress feedback.\n\nAvailable commands:\n add Add a new backup strategy (source -> destination)\n rm Remove an existing backup strategy\n all Show all configured backup strategies\n save Execute backup tasks\n version Show version information", + "cli.epilog": "Examples:\n Add backup strategy:\n sbackup add F:/my_folder F:/backup -i node_modules,.git\n\n Add strategy with entry-level format:\n sbackup add F:/my_folder F:/backup --format tar.gz\n\n Remove backup strategy:\n sbackup rm F:/my_folder\n\n View all strategies:\n sbackup all\n\n Execute backup (ZIP):\n sbackup save\n\n Execute backup (tar.gz):\n sbackup --format tar.gz save\n\n Execute backup (7z encrypted):\n sbackup --format 7z save --password 123456\n\n Keep last 5 backups:\n sbackup save --keep 5\n\n Watch backup (every 60 min):\n sbackup watch --interval 60\n\n Watch + cleanup:\n sbackup watch --interval 120 --keep 10\n\n Restore backup:\n sbackup restore backup.7z F:/restored\n\n Chinese interface:\n sbackup --lang zh_CN save\n\n Debug mode:\n sbackup --debug save", + "cli.help.debug": "Enable debug mode with detailed logging", + "cli.help.help": "Show this help message and exit", + "cli.help.lang": "Set interface language: zh_CN / en_US / fr_FR / es_ES / ru_RU / de_DE / ja_JP / pt_BR / ko_KR", + "cli.help.format": "Set packaging format: zip (default) / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.subcommands": "Select a command to execute", + "cli.help.add": "Add a new backup strategy", + "cli.help.add.source": "Source folder path: the directory to be backed up", + "cli.help.add.dest": "Target folder path: where backup files will be stored", + "cli.help.add.ignore": "Files or folders to ignore (comma separated, default: .git,__pycache__)", + "cli.help.add.format": "Entry-level packaging format (only for this strategy, defaults to global setting): zip / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.rm": "Remove a backup strategy", + "cli.help.rm.path": "Source folder path of the strategy to remove", + "cli.help.all": "View all backup strategies", + "cli.help.save": "Execute all backup strategies", + "cli.help.save.keep": "Keep the most recent N backup files, 0 to disable cleanup", + "cli.help.save.password": "Encryption password (only supported for 7z format)", + "cli.help.watch": "Run backups periodically", + "cli.help.watch.interval": "Backup interval in minutes, default 60", + "cli.help.watch.keep": "Keep the most recent N backup files", + "cli.help.watch.password": "Encryption password (only supported for 7z format)", + "cli.help.restore": "Restore from a backup file", + "cli.help.restore.file": "Backup file path (.zip / .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .7z)", + "cli.help.restore.dir": "Target directory for restoration", + "cli.help.version": "Show version information", + "cli.version": "\nSbackup v{version} — Copyright © 2026 xiatianxuan\nLicensed under GNU GPL v3.0 — https://www.gnu.org/licenses/gpl-3.0.html\n", + + "log.config.malformed": "Config file %s is malformed, using defaults", + "log.config.reset": "Config file %s is malformed, resetting to empty", + "log.config.mkdir.error": "Cannot create config directory %s: %s", + "log.config.write.error": "Cannot write config file %s: %s", + "log.data.read": "Reading data file: %s", + "log.data.create": "Data file does not exist, creating: %s", + "log.data.load": "Loading existing data file: %s", + "log.data.write": "Writing data file: %s", + "log.cleanup.delete": "Deleted old backup: %s", + "log.i18n.load.failed": "Failed to load locale {path}: {error}", + "log.i18n.load.fallback_failed": "Failed to load fallback locale {path}: {error}" +} diff --git a/sbackup/locales/es_ES.json b/sbackup/locales/es_ES.json new file mode 100644 index 0000000..e1e074f --- /dev/null +++ b/sbackup/locales/es_ES.json @@ -0,0 +1,80 @@ +{ + "cmd.add.success": "Estrategia de copia de seguridad añadida con éxito: {source} -> {dest}", + "cmd.rm.success": "Estrategia de copia de seguridad eliminada con éxito: {path}", + "err.folder.invalid": "Error: {path} no es una carpeta válida o no existe.", + "err.dest.invalid": "Error: La carpeta destino {path} no es una carpeta válida o no existe.", + "info.already.added": "Info: {path} ya ha sido añadido. No lo duplique.", + "warn.no.strategy.found": "Advertencia: Estrategia de copia de seguridad para {path} no encontrada.", + "warn.source.missing": "Advertencia: La carpeta fuente no existe: {path}", + "warn.json.decode.error": "Advertencia: El archivo de datos {path} está malformado, inicializando como diccionario vacío.", + "warn.json.backup": "Advertencia: El archivo de datos corrupto se ha respaldado en {path}.", + "warn.json.renamed": "Advertencia: El archivo de datos corrupto se ha renombrado a {path}.", + "compress.success": "Copia de seguridad exitosa: {path} ({size:.2f} MB, {count} archivos)", + "compress.progress": "Comprimiendo", + "compress.unit": "archivos", + "err.permission": "Permiso denegado: No se puede escribir en '{path}'", + "err.os": "Error del sistema: {error}", + "err.unknown": "Error desconocido: {error}", + "err.unknown.format": "Error: Format de archivo de copia de seguridad no reconocido {path}", + "err.argparse.invalid_choice": "opción no válida: ", + "err.argparse.choose_from": "elegir entre", + "err.argparse.invalid_float": "valor de tipo float no válido: ", + "err.argparse.invalid_int": "valor de tipo int no válido: ", + "err.argparse.unrecognized_args": "argumentos no reconocidos: ", + "err.argparse.required": "requerido", + "warn.zip.overwrite": "Advertencia: Sobrescribiendo el archivo de copia de seguridad existente {path}", + "warn.invalid.compresslevel": "Advertencia: Nivel de compresión {level} fuera de rango (0-9), usando el valor predeterminado 6", + "cmd.save.uptodate": "Todas las estrategias de copia de seguridad están actualizadas, no se necesita copia.", + "cmd.save.completed": "Copia de seguridad completada, {count} estrategia(s) procesada(s).", + "cmd.all.empty": "No hay estrategias de copia de seguridad configuradas.", + "exit.message": "\nSaliendo.", + "table.header.source": "Carpeta fuente", + "table.header.dest": "Carpeta destino", + "table.header.ignore": "Patrones de exclusión", + "table.cell.none": "Ninguno", + "table.header.format": "Formato", + "table.cell.default": "Predet.", + "restore.progress": "Restaurando", + "restore.success": "Restauración exitosa: {path} ({count} archivos)", + "cmd.watch.start": "Monitorización iniciada, intervalo {interval} min (pulse Ctrl+C para detener)", + + "cli.description": "Sbackup v{version} — Herramienta de copia de seguridad incremental ligera\n\nSbackup le ayuda a gestionar fácilmente las copias de seguridad de múltiples carpetas.\nUtiliza una estrategia de copia de seguridad incremental, comprimiendo únicamente los archivos modificados,\nsoportando reglas de exclusión personalizadas y ofreciendo una retroalimentación de progresión intuitiva.\n\nComandos disponibles :\n add Añadir una nueva estrategia de copia de seguridad (fuente -> destino)\n rm Eliminar una estrategia de copia de seguridad existente\n all Mostrar todas las estrategias de copia de seguridad configuradas\n save Ejecutar las tareas de copia de seguridad\n version Mostrar la información de versión", + "cli.epilog": "Ejemplos :\n Añadir una estrategia de copia de seguridad :\n sbackup add F:/my_folder F:/backup -i node_modules,.git\n\n Añadir estrategia con formato por entrada:\n sbackup add F:/my_folder F:/backup --format tar.gz\n\n Eliminar una estrategia de copia de seguridad :\n sbackup rm F:/my_folder\n\n Mostrar todas las estrategias :\n sbackup all\n\n Ejecutar una copia de seguridad (ZIP) :\n sbackup save\n\n Ejecutar una copia de seguridad (tar.gz) :\n sbackup --format tar.gz save\n\n Ejecutar una copia de seguridad (7z cifrado) :\n sbackup --format 7z save --password 123456\n\n Conservar las 5 últimas copias de seguridad :\n sbackup save --keep 5\n\n Copia de seguridad programada (cada 60 min) :\n sbackup watch --interval 60\n\n Monitorización + limpieza :\n sbackup watch --interval 120 --keep 10\n\n Restaurar una copia de seguridad :\n sbackup restore backup.7z F:/restored\n\n Interfaz en chino :\n sbackup --lang zh_CN save\n\n Modo depuración :\n sbackup --debug save", + "cli.help.debug": "Activar el modo depuración con registro detallado", + "cli.help.help": "Mostrar este mensaje de ayuda y salir", + "cli.help.lang": "Definir el idioma de la interfaz: zh_CN / en_US / fr_FR / es_ES / ru_RU / de_DE / ja_JP / pt_BR / ko_KR", + "cli.help.format": "Definir el formato de archivado: zip (predet.) / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.subcommands": "Seleccionar un comando para ejecutar", + "cli.help.add": "Añadir una nueva estrategia de copia de seguridad", + "cli.help.add.source": "Ruta de la carpeta fuente: el directorio a copiar", + "cli.help.add.dest": "Ruta de la carpeta destino: donde se almacenarán los archivos de copia de seguridad", + "cli.help.add.ignore": "Archivos o carpetas a ignorar (separados por comas, predet.: .git,__pycache__)", + "cli.help.add.format": "Formato de archivado por entrada (solo para esta estrategia, por defecto global): zip / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.rm": "Eliminar una estrategia de copia de seguridad", + "cli.help.rm.path": "Ruta de la carpeta fuente de la estrategia a eliminar", + "cli.help.all": "Mostrar todas las estrategias de copia de seguridad", + "cli.help.save": "Ejecutar todas las estrategias de copia de seguridad", + "cli.help.save.keep": "Conservar los N últimos archivos de copia de seguridad, 0 para desactivar la limpieza", + "cli.help.save.password": "Contraseña de cifrado (solo para el formato 7z)", + "cli.help.watch": "Ejecutar copias de seguridad periódicamente", + "cli.help.watch.interval": "Intervalo de copia de seguridad en minutos, predet. 60", + "cli.help.watch.keep": "Conservar los N últimos archivos de copia de seguridad", + "cli.help.watch.password": "Contraseña de cifrado (solo para el formato 7z)", + "cli.help.restore": "Restaurar desde un archivo de copia de seguridad", + "cli.help.restore.file": "Ruta del archivo de copia de seguridad (.zip / .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .7z)", + "cli.help.restore.dir": "Directorio destino para la restauración", + "cli.help.version": "Mostrar la información de versión", + "cli.version": "\nSbackup v{version} — Copyright © 2026 xiatianxuan\nLicencia GNU GPL v3.0 — https://www.gnu.org/licenses/gpl-3.0.html\n", + + "log.config.malformed": "El archivo de configuración %s está malformado, usando valores predeterminados", + "log.config.reset": "El archivo de configuración %s está malformado, reiniciando", + "log.config.mkdir.error": "No se puede crear el directorio de configuración %s : %s", + "log.config.write.error": "No se puede escribir el archivo de configuración %s : %s", + "log.data.read": "Leyendo el archivo de datos : %s", + "log.data.create": "El archivo de datos no existe, creando : %s", + "log.data.load": "Cargando el archivo de datos existente : %s", + "log.data.write": "Escribiendo el archivo de datos : %s", + "log.cleanup.delete": "Copia de seguridad antigua eliminada : %s", + "log.i18n.load.failed": "No se pudo cargar el locale {path}: {error}", + "log.i18n.load.fallback_failed": "No se pudo cargar el locale de respaldo {path}: {error}" +} \ No newline at end of file diff --git a/sbackup/locales/fr_FR.json b/sbackup/locales/fr_FR.json new file mode 100644 index 0000000..89db7af --- /dev/null +++ b/sbackup/locales/fr_FR.json @@ -0,0 +1,80 @@ +{ + "cmd.add.success": "Stratégie de sauvegarde ajoutée avec succès : {source} -> {dest}", + "cmd.rm.success": "Stratégie de sauvegarde supprimée avec succès : {path}", + "err.folder.invalid": "Erreur : {path} n'est pas un dossier valide ou n'existe pas.", + "err.dest.invalid": "Erreur : Le dossier cible {path} n'est pas un dossier valide ou n'existe pas.", + "info.already.added": "Info : {path} a déjà été ajouté. Veuillez ne pas dupliquer.", + "warn.no.strategy.found": "Attention : Stratégie de sauvegarde pour {path} non trouvée.", + "warn.source.missing": "Attention : Le dossier source n'existe pas : {path}", + "warn.json.decode.error": "Attention : Le fichier de données {path} est malformé, initialisation en dict vide.", + "warn.json.backup": "Attention : Le fichier de données corrompu a été sauvegardé dans {path}.", + "warn.json.renamed": "Attention : Le fichier de données corrompu a été renommé en {path}.", + "compress.success": "Sauvegarde réussie : {path} ({size:.2f} MB, {count} fichiers)", + "compress.progress": "Compression en cours", + "compress.unit": "fichiers", + "err.permission": "Accès refusé : Impossible d'écrire dans '{path}'", + "err.os": "Erreur système : {error}", + "err.unknown": "Erreur inconnue : {error}", + "err.unknown.format": "Erreur : Format de fichier de sauvegarde non reconnu {path}", + "err.argparse.invalid_choice": "choix invalide : ", + "err.argparse.choose_from": "choisir parmi", + "err.argparse.invalid_float": "valeur flottante invalide : ", + "err.argparse.invalid_int": "valeur entière invalide : ", + "err.argparse.unrecognized_args": "arguments non reconnus : ", + "err.argparse.required": "requis", + "warn.zip.overwrite": "Attention : Remplacement du fichier de sauvegarde existant {path}", + "warn.invalid.compresslevel": "Attention : Niveau de compression {level} hors plage (0-9), utilisation de la valeur par défaut 6", + "cmd.save.uptodate": "Toutes les stratégies de sauvegarde sont à jour, aucune sauvegarde nécessaire.", + "cmd.save.completed": "Sauvegarde terminée, {count} stratégie(s) traitée(s).", + "cmd.all.empty": "Aucune stratégie de sauvegarde configurée.", + "exit.message": "\nSortie.", + "table.header.source": "Dossier source", + "table.header.dest": "Dossier cible", + "table.header.ignore": "Modèles d'exclusion", + "table.cell.none": "Aucun", + "table.header.format": "Format", + "table.cell.default": "Défaut", + "restore.progress": "Restauration en cours", + "restore.success": "Restauration réussie : {path} ({count} fichiers)", + "cmd.watch.start": "Surveillance démarrée, intervalle {interval} min (appuyez sur Ctrl+C pour arrêter)", + + "cli.description": "Sbackup v{version} — Outil de sauvegarde incrémentielle léger\n\nSbackup vous aide à gérer facilement les sauvegardes de plusieurs dossiers.\nIl utilise une stratégie de sauvegarde incrémentielle, compressant uniquement les fichiers modifiés,\nsupportant des règles d'exclusion personnalisées et offrant un retour de progression intuitif.\n\nCommandes disponibles :\n add Ajouter une nouvelle stratégie de sauvegarde (source -> destination)\n rm Supprimer une stratégie de sauvegarde existante\n all Afficher toutes les stratégies de sauvegarde configurées\n save Exécuter les tâches de sauvegarde\n version Afficher les informations de version", + "cli.epilog": "Exemples :\n Ajouter une stratégie de sauvegarde :\n sbackup add F:/my_folder F:/backup -i node_modules,.git\n\n Ajouter une stratégie avec format au niveau de l'entrée :\n sbackup add F:/my_folder F:/backup --format tar.gz\n\n Supprimer une stratégie de sauvegarde :\n sbackup rm F:/my_folder\n\n Afficher toutes les stratégies :\n sbackup all\n\n Exécuter une sauvegarde (ZIP) :\n sbackup save\n\n Exécuter une sauvegarde (tar.gz) :\n sbackup --format tar.gz save\n\n Exécuter une sauvegarde (7z chiffré) :\n sbackup --format 7z save --password 123456\n\n Conserver les 5 dernières sauvegardes :\n sbackup save --keep 5\n\n Sauvegarde planifiée (toutes les 60 min) :\n sbackup watch --interval 60\n\n Surveillance + nettoyage :\n sbackup watch --interval 120 --keep 10\n\n Restaurer une sauvegarde :\n sbackup restore backup.7z F:/restored\n\n Interface en chinois :\n sbackup --lang zh_CN save\n\n Mode débogage :\n sbackup --debug save", + "cli.help.debug": "Activer le mode débogage avec journalisation détaillée", + "cli.help.help": "Afficher ce message d'aide et quitter", + "cli.help.lang": "Définir la langue de l'interface : zh_CN / en_US / fr_FR / es_ES / ru_RU / de_DE / ja_JP / pt_BR / ko_KR", + "cli.help.format": "Définir le format d'archivage : zip (défaut) / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.subcommands": "Sélectionner une commande à exécuter", + "cli.help.add": "Ajouter une nouvelle stratégie de sauvegarde", + "cli.help.add.source": "Chemin du dossier source : le répertoire à sauvegarder", + "cli.help.add.dest": "Chemin du dossier cible : où les fichiers de sauvegarde seront stockés", + "cli.help.add.ignore": "Fichiers ou dossiers à ignorer (séparés par des virgules, défaut : .git,__pycache__)", + "cli.help.add.format": "Format d'archivage au niveau de l'entrée (uniquement pour cette stratégie, par défaut global) : zip / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.rm": "Supprimer une stratégie de sauvegarde", + "cli.help.rm.path": "Chemin du dossier source de la stratégie à supprimer", + "cli.help.all": "Afficher toutes les stratégies de sauvegarde", + "cli.help.save": "Exécuter toutes les stratégies de sauvegarde", + "cli.help.save.keep": "Conserver les N derniers fichiers de sauvegarde, 0 pour désactiver le nettoyage", + "cli.help.save.password": "Mot de passe de chiffrement (uniquement pour le format 7z)", + "cli.help.watch": "Exécuter les sauvegardes périodiquement", + "cli.help.watch.interval": "Intervalle de sauvegarde en minutes, défaut 60", + "cli.help.watch.keep": "Conserver les N derniers fichiers de sauvegarde", + "cli.help.watch.password": "Mot de passe de chiffrement (uniquement pour le format 7z)", + "cli.help.restore": "Restaurer depuis un fichier de sauvegarde", + "cli.help.restore.file": "Chemin du fichier de sauvegarde (.zip / .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .7z)", + "cli.help.restore.dir": "Dossier cible pour la restauration", + "cli.help.version": "Afficher les informations de version", + "cli.version": "\nSbackup v{version} — Copyright © 2026 xiatianxuan\nLicence GNU GPL v3.0 — https://www.gnu.org/licenses/gpl-3.0.html\n", + + "log.config.malformed": "Le fichier de configuration %s est malformé, utilisation des valeurs par défaut", + "log.config.reset": "Le fichier de configuration %s est malformé, réinitialisation", + "log.config.mkdir.error": "Impossible de créer le dossier de configuration %s : %s", + "log.config.write.error": "Impossible d'écrire le fichier de configuration %s : %s", + "log.data.read": "Lecture du fichier de données : %s", + "log.data.create": "Le fichier de données n'existe pas, création : %s", + "log.data.load": "Chargement du fichier de données existant : %s", + "log.data.write": "Écriture du fichier de données : %s", + "log.cleanup.delete": "Ancienne sauvegarde supprimée : %s", + "log.i18n.load.failed": "Impossible de charger le locale {path} : {error}", + "log.i18n.load.fallback_failed": "Impossible de charger le locale de repli {path} : {error}" +} \ No newline at end of file diff --git a/sbackup/locales/ja_JP.json b/sbackup/locales/ja_JP.json new file mode 100644 index 0000000..f99b9aa --- /dev/null +++ b/sbackup/locales/ja_JP.json @@ -0,0 +1,80 @@ +{ + "cmd.add.success": "バックアップ戦略を追加しました: {source} -> {dest}", + "cmd.rm.success": "バックアップ戦略を削除しました: {path}", + "err.folder.invalid": "エラー: {path} は有効なフォルダではないか、存在しません。", + "err.dest.invalid": "エラー: 目標フォルダ {path} は有効なフォルダではないか、存在しません。", + "info.already.added": "情報: {path} は既に追加されています。重複して追加しないでください。", + "warn.no.strategy.found": "警告: {path} のバックアップ戦略が見つかりません。", + "warn.source.missing": "警告: ソースフォルダが存在しません: {path}", + "warn.json.decode.error": "警告: データファイル {path} が破損しています、空の辞書として初期化します。", + "warn.json.backup": "警告: 破損したデータファイルを {path} にバックアップしました。", + "warn.json.renamed": "警告: 破損したデータファイルを {path} にリネームしました。", + "compress.success": "バックアップ成功: {path} ({size:.2f} MB、{count} ファイル)", + "compress.progress": "圧縮中", + "compress.unit": "ファイル", + "err.permission": "アクセス拒否: '{path}' に書き込みできません", + "err.os": "システムエラー: {error}", + "err.unknown": "不明なエラー: {error}", + "err.unknown.format": "エラー: 不明なバックアップファイル形式 {path}", + "err.argparse.invalid_choice": "無効な選択: ", + "err.argparse.choose_from": "選択可能", + "err.argparse.invalid_float": "無効な浮動小数点値: ", + "err.argparse.invalid_int": "無効な整数値: ", + "err.argparse.unrecognized_args": "未認識の引数: ", + "err.argparse.required": "必須", + "warn.zip.overwrite": "警告: 既存のバックアップファイル {path} を上書きします", + "warn.invalid.compresslevel": "警告: 圧縮レベル {level} が範囲外 (0-9)、デフォルト値 6 を使用します", + "cmd.save.uptodate": "すべてのバックアップ戦略は最新です、バックアップは不要です。", + "cmd.save.completed": "バックアップ完了、{count} 戦略を処理しました。", + "cmd.all.empty": "バックアップ戦略が設定されていません。", + "exit.message": "\n終了します。", + "table.header.source": "ソースフォルダ", + "table.header.dest": "目標フォルダ", + "table.header.ignore": "除外パターン", + "table.cell.none": "なし", + "table.header.format": "形式", + "table.cell.default": "デフォルト", + "restore.progress": "復元中", + "restore.success": "復元成功: {path} ({count} ファイル)", + "cmd.watch.start": "監視開始、間隔 {interval} 分(Ctrl+C で停止)", + + "cli.description": "Sbackup v{version} — 軽量インクリメンタルバックアップツール\n\nSbackup は複数フォルダのバックアップタスクを簡単に管理します。\nインクリメンタルバックアップ戦略を採用し、変更されたファイルのみを圧縮し、\nカスタム除外ルールをサポートし、直感的な進捗フィードバックを提供します。\n\n使用可能なコマンド:\n add 新しいバックアップ戦略を追加(ソース -> 目標)\n rm 既存のバックアップ戦略を削除\n all 設定済みのすべてのバックアップ戦略を表示\n save バックアップタスクを実行\n version バージョン情報を表示", + "cli.epilog": "例:\n バックアップ戦略を追加:\n sbackup add F:/my_folder F:/backup -i node_modules,.git\n\n エントリレベル形式を指定して戦略を追加:\n sbackup add F:/my_folder F:/backup --format tar.gz\n\n バックアップ戦略を削除:\n sbackup rm F:/my_folder\n\n すべての戦略を表示:\n sbackup all\n\n バックアップを実行 (ZIP):\n sbackup save\n\n バックアップを実行 (tar.gz):\n sbackup --format tar.gz save\n\n バックアップを実行 (7z 暗号化):\n sbackup --format 7z save --password 123456\n\n 最新5件のバックアップを保持:\n sbackup save --keep 5\n\n 定期バックアップ (60分間隔):\n sbackup watch --interval 60\n\n 監視 + クリーンアップ:\n sbackup watch --interval 120 --keep 10\n\n バックアップを復元:\n sbackup restore backup.7z F:/restored\n\n 中国語インターフェース:\n sbackup --lang zh_CN save\n\n デバッグモード:\n sbackup --debug save", + "cli.help.debug": "詳細なログ出力付きデバッグモードを有効化", + "cli.help.help": "このヘルプメッセージを表示して終了", + "cli.help.lang": "インターフェース言語を設定: zh_CN / en_US / fr_FR / es_ES / ru_RU / de_DE / ja_JP / pt_BR / ko_KR", + "cli.help.format": "パッケージ形式を設定: zip (デフォルト) / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.subcommands": "実行するコマンドを選択", + "cli.help.add": "新しいバックアップ戦略を追加", + "cli.help.add.source": "ソースフォルダパス: バックアップするディレクトリ", + "cli.help.add.dest": "目標フォルダパス: バックアップファイルの保存場所", + "cli.help.add.ignore": "除外するファイルやフォルダ (カンマ区切り、デフォルト: .git,__pycache__)", + "cli.help.add.format": "エントリレベルのパッケージ形式(この戦略のみ、未指定時はグローバル設定): zip / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.rm": "バックアップ戦略を削除", + "cli.help.rm.path": "削除する戦略のソースフォルダパス", + "cli.help.all": "すべてのバックアップ戦略を表示", + "cli.help.save": "すべてのバックアップ戦略を実行", + "cli.help.save.keep": "最新 N 件のバックアップファイルを保持、0 でクリーンアップ無効", + "cli.help.save.password": "暗号化パスワード (7z形式のみ)", + "cli.help.watch": "定期的にバックアップを実行", + "cli.help.watch.interval": "バックアップ間隔 (分)、デフォルト 60", + "cli.help.watch.keep": "最新 N 件のバックアップファイルを保持", + "cli.help.watch.password": "暗号化パスワード (7z形式のみ)", + "cli.help.restore": "バックアップファイルから復元", + "cli.help.restore.file": "バックアップファイルパス (.zip / .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .7z)", + "cli.help.restore.dir": "復元目標ディレクトリ", + "cli.help.version": "バージョン情報を表示", + "cli.version": "\nSbackup v{version} — Copyright © 2026 xiatianxuan\nGNU GPL v3.0 ライセンス — https://www.gnu.org/licenses/gpl-3.0.html\n", + + "log.config.malformed": "設定ファイル %s が破損しています、デフォルト値を使用", + "log.config.reset": "設定ファイル %s が破損しています、リセット", + "log.config.mkdir.error": "設定ディレクトリ %s を作成できません: %s", + "log.config.write.error": "設定ファイル %s を書き込みできません: %s", + "log.data.read": "データファイルを読み込み: %s", + "log.data.create": "データファイルが存在しません、作成: %s", + "log.data.load": "既存のデータファイルを読み込み: %s", + "log.data.write": "データファイルを書き込み: %s", + "log.cleanup.delete": "古いバックアップを削除しました: %s", + "log.i18n.load.failed": "ロケール {path} の読み込みに失敗: {error}", + "log.i18n.load.fallback_failed": "フォールバックロケール {path} の読み込みに失敗: {error}" +} \ No newline at end of file diff --git a/sbackup/locales/ko_KR.json b/sbackup/locales/ko_KR.json new file mode 100644 index 0000000..f5e97ae --- /dev/null +++ b/sbackup/locales/ko_KR.json @@ -0,0 +1,80 @@ +{ + "cmd.add.success": "백업 전략 추가 성공: {source} -> {dest}", + "cmd.rm.success": "백업 전략 삭제 성공: {path}", + "err.folder.invalid": "오류: {path}는 유효한 폴더가 아니거나 존재하지 않습니다.", + "err.dest.invalid": "오류: 대상 폴더 {path}는 유효한 폴더가 아니거나 존재하지 않습니다.", + "info.already.added": "알림: {path}가 이미 추가되었습니다. 중복 추가하지 마세요.", + "warn.no.strategy.found": "경고: {path}의 백업 전략을 찾을 수 없습니다.", + "warn.source.missing": "경고: 원본 폴더가 존재하지 않습니다: {path}", + "warn.json.decode.error": "경고: 데이터 파일 {path}가 손상되었습니다, 빈 딕셔너리로 초기화합니다.", + "warn.json.backup": "경고: 손상된 데이터 파일을 {path}에 백업했습니다.", + "warn.json.renamed": "경고: 손상된 데이터 파일을 {path}로 이름 변경했습니다.", + "compress.success": "백업 성공: {path} ({size:.2f} MB, {count} 파일)", + "compress.progress": "압축 중", + "compress.unit": "파일", + "err.permission": "접근 거부: '{path}'에 쓸 수 없습니다", + "err.os": "시스템 오류: {error}", + "err.unknown": "알 수 없는 오류: {error}", + "err.unknown.format": "오류: 인식할 수 없는 백업 파일 형식 {path}", + "err.argparse.invalid_choice": "잘못된 선택: ", + "err.argparse.choose_from": "선택 가능", + "err.argparse.invalid_float": "잘못된 실수 값: ", + "err.argparse.invalid_int": "잘못된 정수 값: ", + "err.argparse.unrecognized_args": "인식되지 않은 인수: ", + "err.argparse.required": "필수", + "warn.zip.overwrite": "경고: 기존 백업 파일 {path}을 덮어씁니다", + "warn.invalid.compresslevel": "경고: 압축 수준 {level}이 범위 초과 (0-9), 기본값 6 사용", + "cmd.save.uptodate": "모든 백업 전략이 최신 상태입니다, 백업이 필요하지 않습니다.", + "cmd.save.completed": "백업 완료, {count}개 전략 처리됨.", + "cmd.all.empty": "구성된 백업 전략이 없습니다.", + "exit.message": "\n종료합니다.", + "table.header.source": "원본 폴더", + "table.header.dest": "대상 폴더", + "table.header.ignore": "제외 패턴", + "table.cell.none": "없음", + "table.header.format": "형식", + "table.cell.default": "기본값", + "restore.progress": "복원 중", + "restore.success": "복원 성공: {path} ({count} 파일)", + "cmd.watch.start": "감시 시작, 간격 {interval}분 (Ctrl+C로 중지)", + + "cli.description": "Sbackup v{version} — 경량 증분 백업 도구\n\nSbackup는 여러 폴더의 백업 작업을 쉽게 관리합니다.\n증분 백업 전략을 사용하여 변경된 파일만 압축하고,\n사용자 정의 제외 규칙을 지원하며 직관적인 진행 피드백을 제공합니다.\n\n사용 가능한 명령:\n add 새 백업 전략 추가 (원본 -> 대상)\n rm 기존 백업 전략 삭제\n all 구성된 모든 백업 전략 표시\n save 백업 작업 실행\n version 버전 정보 표시", + "cli.epilog": "예시:\n 백업 전략 추가:\n sbackup add F:/my_folder F:/backup -i node_modules,.git\n\n 항목별 형식 지정하여 전략 추가:\n sbackup add F:/my_folder F:/backup --format tar.gz\n\n 백업 전략 삭제:\n sbackup rm F:/my_folder\n\n 모든 전략 표시:\n sbackup all\n\n 백업 실행 (ZIP):\n sbackup save\n\n 백업 실행 (tar.gz):\n sbackup --format tar.gz save\n\n 백업 실행 (7z 암호화):\n sbackup --format 7z save --password 123456\n\n 최근 5개 백업 유지:\n sbackup save --keep 5\n\n 정기 백업 (60분 간격):\n sbackup watch --interval 60\n\n 감시 + 정리:\n sbackup watch --interval 120 --keep 10\n\n 백업 복원:\n sbackup restore backup.7z F:/restored\n\n 중국어 인터페이스:\n sbackup --lang zh_CN save\n\n 디버그 모드:\n sbackup --debug save", + "cli.help.debug": "상세 로깅 디버그 모드 활성화", + "cli.help.help": "이 도움말 메시지를 표시하고 종료", + "cli.help.lang": "인터페이스 언어 설정: zh_CN / en_US / fr_FR / es_ES / ru_RU / de_DE / ja_JP / pt_BR / ko_KR", + "cli.help.format": "패키징 형식 설정: zip (기본값) / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.subcommands": "실행할 명령 선택", + "cli.help.add": "새 백업 전략 추가", + "cli.help.add.source": "원본 폴더 경로: 백업할 디렉토리", + "cli.help.add.dest": "대상 폴더 경로: 백업 파일이 저장될 위치", + "cli.help.add.ignore": "제외할 파일 또는 폴더 (쉼표로 구분, 기본값: .git,__pycache__)", + "cli.help.add.format": "항목별 패키징 형식 (이 전략만, 미지정시 전역 설정): zip / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.rm": "백업 전략 삭제", + "cli.help.rm.path": "삭제할 전략의 원본 폴더 경로", + "cli.help.all": "모든 백업 전략 표시", + "cli.help.save": "모든 백업 전략 실행", + "cli.help.save.keep": "최근 N개 백업 파일 유지, 0은 정리 비활성화", + "cli.help.save.password": "암호화 비밀번호 (7z 형식만 지원)", + "cli.help.watch": "정기적으로 백업 실행", + "cli.help.watch.interval": "백업 간격 (분), 기본값 60", + "cli.help.watch.keep": "최근 N개 백업 파일 유지", + "cli.help.watch.password": "암호화 비밀번호 (7z 형식만 지원)", + "cli.help.restore": "백업 파일에서 복원", + "cli.help.restore.file": "백업 파일 경로 (.zip / .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .7z)", + "cli.help.restore.dir": "복원 대상 디렉토리", + "cli.help.version": "버전 정보 표시", + "cli.version": "\nSbackup v{version} — Copyright © 2026 xiatianxuan\nGNU GPL v3.0 라이선스 — https://www.gnu.org/licenses/gpl-3.0.html\n", + + "log.config.malformed": "구성 파일 %s가 손상되었습니다, 기본값 사용", + "log.config.reset": "구성 파일 %s가 손상되었습니다, 재설정", + "log.config.mkdir.error": "구성 디렉토리 %s를 생성할 수 없습니다: %s", + "log.config.write.error": "구성 파일 %s를 쓸 수 없습니다: %s", + "log.data.read": "데이터 파일 읽기: %s", + "log.data.create": "데이터 파일이 없습니다, 생성: %s", + "log.data.load": "기존 데이터 파일 로드: %s", + "log.data.write": "데이터 파일 쓰기: %s", + "log.cleanup.delete": "오래된 백업 삭제됨: %s", + "log.i18n.load.failed": "로케일 {path} 로드 실패: {error}", + "log.i18n.load.fallback_failed": "폴백 로케일 {path} 로드 실패: {error}" +} \ No newline at end of file diff --git a/sbackup/locales/pt_BR.json b/sbackup/locales/pt_BR.json new file mode 100644 index 0000000..227d2cb --- /dev/null +++ b/sbackup/locales/pt_BR.json @@ -0,0 +1,80 @@ +{ + "cmd.add.success": "Estratégia de backup adicionada com sucesso: {source} -> {dest}", + "cmd.rm.success": "Estratégia de backup removida com sucesso: {path}", + "err.folder.invalid": "Erro: {path} não é uma pasta válida ou não existe.", + "err.dest.invalid": "Erro: A pasta de destino {path} não é uma pasta válida ou não existe.", + "info.already.added": "Info: {path} já foi adicionado. Não duplique.", + "warn.no.strategy.found": "Aviso: Estratégia de backup para {path} não encontrada.", + "warn.source.missing": "Aviso: A pasta de origem não existe: {path}", + "warn.json.decode.error": "Aviso: Arquivo de dados {path} malformado, inicializando como diccionário vazio.", + "warn.json.backup": "Aviso: Arquivo de dados corrompido copiado para {path}.", + "warn.json.renamed": "Aviso: Arquivo de dados corrompido renomeado para {path}.", + "compress.success": "Backup realizado com sucesso: {path} ({size:.2f} MB, {count} arquivos)", + "compress.progress": "Comprimindo", + "compress.unit": "arquivos", + "err.permission": "Permissão negada: Não é possível escrever em '{path}'", + "err.os": "Erro do sistema: {error}", + "err.unknown": "Erro desconhecido: {error}", + "err.unknown.format": "Erro: Formato de arquivo de backup não reconhecido {path}", + "err.argparse.invalid_choice": "opção inválida: ", + "err.argparse.choose_from": "escolher entre", + "err.argparse.invalid_float": "valor float inválido: ", + "err.argparse.invalid_int": "valor int inválido: ", + "err.argparse.unrecognized_args": "argumentos não reconhecidos: ", + "err.argparse.required": "obrigatório", + "warn.zip.overwrite": "Aviso: Sobrescrevendo arquivo de backup existente {path}", + "warn.invalid.compresslevel": "Aviso: Nível de compressão {level} fora do intervalo (0-9), usando valor padrão 6", + "cmd.save.uptodate": "Todas as estratégias de backup estão atualizadas, backup não necessário.", + "cmd.save.completed": "Backup concluído, {count} estratégia(s) processada(s).", + "cmd.all.empty": "Nenhuma estratégia de backup configurada.", + "exit.message": "\nSaindo.", + "table.header.source": "Pasta de origem", + "table.header.dest": "Pasta de destino", + "table.header.ignore": "Padrões de exclusão", + "table.cell.none": "Nenhum", + "table.header.format": "Formato", + "table.cell.default": "Padrão", + "restore.progress": "Restaurando", + "restore.success": "Restauração concluída: {path} ({count} arquivos)", + "cmd.watch.start": "Monitoramento iniciado, intervalo {interval} min (Ctrl+C para parar)", + + "cli.description": "Sbackup v{version} — Ferramenta de backup incremental leve\n\nSbackup ajuda você a gerenciar facilmente backups de múltiplas pastas.\nEle usa uma estratégia de backup incremental, comprimindo apenas arquivos modificados,\nsuportando regras de exclusão personalizadas e oferecendo feedback de progresso intuitivo.\n\nComandos disponíveis:\n add Adicionar nova estratégia de backup (origem -> destino)\n rm Remover estratégia de backup existente\n all Mostrar todas as estratégias de backup configuradas\n save Executar tarefas de backup\n version Mostrar informações de versão", + "cli.epilog": "Exemplos:\n Adicionar estratégia de backup:\n sbackup add F:/my_folder F:/backup -i node_modules,.git\n\n Adicionar estratégia com formato por entrada:\n sbackup add F:/my_folder F:/backup --format tar.gz\n\n Remover estratégia de backup:\n sbackup rm F:/my_folder\n\n Mostrar todas as estratégias:\n sbackup all\n\n Executar backup (ZIP):\n sbackup save\n\n Executar backup (tar.gz):\n sbackup --format tar.gz save\n\n Executar backup (7z criptografado):\n sbackup --format 7z save --password 123456\n\n Manter os últimos 5 backups:\n sbackup save --keep 5\n\n Backup programado (cada 60 min):\n sbackup watch --interval 60\n\n Monitoramento + limpeza:\n sbackup watch --interval 120 --keep 10\n\n Restaurar backup:\n sbackup restore backup.7z F:/restored\n\n Interface em chinês:\n sbackup --lang zh_CN save\n\n Modo de depuração:\n sbackup --debug save", + "cli.help.debug": "Ativar modo de depuração com registro detalhado", + "cli.help.help": "Mostrar esta mensagem de ajuda e sair", + "cli.help.lang": "Definir idioma da interface: zh_CN / en_US / fr_FR / es_ES / ru_RU / de_DE / ja_JP / pt_BR / ko_KR", + "cli.help.format": "Definir formato de pacote: zip (padrão) / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.subcommands": "Selecionar um comando para executar", + "cli.help.add": "Adicionar nova estratégia de backup", + "cli.help.add.source": "Caminho da pasta de origem: diretório a ser copiado", + "cli.help.add.dest": "Caminho da pasta de destino: onde os arquivos de backup serão armazenados", + "cli.help.add.ignore": "Arquivos ou pastas a ignorar (separados por vírgula, padrão: .git,__pycache__)", + "cli.help.add.format": "Formato de pacote por entrada (apenas para esta estratégia, padrão global): zip / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.rm": "Remover estratégia de backup", + "cli.help.rm.path": "Caminho da pasta de origem da estratégia a remover", + "cli.help.all": "Mostrar todas as estratégias de backup", + "cli.help.save": "Executar todas as estratégias de backup", + "cli.help.save.keep": "Manter os últimos N arquivos de backup, 0 desativa limpeza", + "cli.help.save.password": "Senha de criptografia (apenas para formato 7z)", + "cli.help.watch": "Executar backups periodicamente", + "cli.help.watch.interval": "Intervalo de backup em minutos, padrão 60", + "cli.help.watch.keep": "Manter os últimos N arquivos de backup", + "cli.help.watch.password": "Senha de criptografia (apenas para formato 7z)", + "cli.help.restore": "Restaurar de um arquivo de backup", + "cli.help.restore.file": "Caminho do arquivo de backup (.zip / .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .7z)", + "cli.help.restore.dir": "Diretório de destino para restauração", + "cli.help.version": "Mostrar informações de versão", + "cli.version": "\nSbackup v{version} — Copyright © 2026 xiatianxuan\nLicença GNU GPL v3.0 — https://www.gnu.org/licenses/gpl-3.0.html\n", + + "log.config.malformed": "Arquivo de configuração %s malformado, usando valores padrão", + "log.config.reset": "Arquivo de configuração %s malformado, redefinindo", + "log.config.mkdir.error": "Não é possível criar diretório de configuração %s: %s", + "log.config.write.error": "Não é possível escrever arquivo de configuração %s: %s", + "log.data.read": "Lendo arquivo de dados: %s", + "log.data.create": "Arquivo de dados não existe, criando: %s", + "log.data.load": "Carregando arquivo de dados existente: %s", + "log.data.write": "Escrevendo arquivo de dados: %s", + "log.cleanup.delete": "Backup antigo excluído: %s", + "log.i18n.load.failed": "Falha ao carregar locale {path}: {error}", + "log.i18n.load.fallback_failed": "Falha ao carregar locale de fallback {path}: {error}" +} \ No newline at end of file diff --git a/sbackup/locales/ru_RU.json b/sbackup/locales/ru_RU.json new file mode 100644 index 0000000..dcf4dd7 --- /dev/null +++ b/sbackup/locales/ru_RU.json @@ -0,0 +1,80 @@ +{ + "cmd.add.success": "Стратегия резервного копирования добавлена: {source} -> {dest}", + "cmd.rm.success": "Стратегия резервного копирования удалена: {path}", + "err.folder.invalid": "Ошибка: {path} не является допустимой папкой или не существует.", + "err.dest.invalid": "Ошибка: Целевая папка {path} не является допустимой папкой или не существует.", + "info.already.added": "Информация: {path} уже добавлен. Не добавляйте повторно.", + "warn.no.strategy.found": "Предупреждение: Стратегия резервного копирования для {path} не найдена.", + "warn.source.missing": "Предупреждение: Исходная папка не существует: {path}", + "warn.json.decode.error": "Предупреждение: Файл данных {path} повреждён, инициализация пустым словарём.", + "warn.json.backup": "Предупреждение: Повреждённый файл данных сохранён в {path}.", + "warn.json.renamed": "Предупреждение: Повреждённый файл данных переименован в {path}.", + "compress.success": "Резервное копирование выполнено: {path} ({size:.2f} МБ, {count} файлов)", + "compress.progress": "Сжатие", + "compress.unit": "файлов", + "err.permission": "Отказ в доступе: Невозможно записать в '{path}'", + "err.os": "Системная ошибка: {error}", + "err.unknown": "Неизвестная ошибка: {error}", + "err.unknown.format": "Ошибка: Неизвестный формат файла резервной копии {path}", + "err.argparse.invalid_choice": "недопустимый выбор: ", + "err.argparse.choose_from": "допустимые значения", + "err.argparse.invalid_float": "недопустимое значение типа float: ", + "err.argparse.invalid_int": "недопустимое целочисленное значение: ", + "err.argparse.unrecognized_args": "неопознанные аргументы: ", + "err.argparse.required": "обязательный", + "warn.zip.overwrite": "Предупреждение: Перезапись существующего файла резервной копии {path}", + "warn.invalid.compresslevel": "Предупреждение: Уровень сжатия {level} вне диапазона (0-9), используется значение 6", + "cmd.save.uptodate": "Все стратегии резервного копирования актуальны, копирование не требуется.", + "cmd.save.completed": "Резервное копирование завершено, обработано {count} стратегий.", + "cmd.all.empty": "Нет настроенных стратегий резервного копирования.", + "exit.message": "\nВыход.", + "table.header.source": "Исходная папка", + "table.header.dest": "Целевая папка", + "table.header.ignore": "Шаблоны исключения", + "table.cell.none": "Нет", + "table.header.format": "Формат", + "table.cell.default": "По умолч.", + "restore.progress": "Восстановление", + "restore.success": "Восстановление выполнено: {path} ({count} файлов)", + "cmd.watch.start": "Автоматическое копирование запущено, интервал {interval} мин (Ctrl+C для остановки)", + + "cli.description": "Sbackup v{version} — Лёгкий инструмент инкрементального резервного копирования\n\nSbackup помогает легко управлять резервным копированием нескольких папок.\nОн использует стратегию инкрементального копирования, сжимая только изменённые файлы,\nподдерживает пользовательские правила исключения и предоставляет наглядный ход выполнения.\n\nДоступные команды:\n add Добавить новую стратегию резервного копирования (источник -> цель)\n rm Удалить существующую стратегию\n all Показать все настроенные стратегии\n save Выполнить резервное копирование\n version Показать информацию о версии", + "cli.epilog": "Примеры:\n Добавить стратегию:\n sbackup add F:/my_folder F:/backup -i node_modules,.git\n\n Добавить стратегию с форматом уровня записи:\n sbackup add F:/my_folder F:/backup --format tar.gz\n\n Удалить стратегию:\n sbackup rm F:/my_folder\n\n Показать все стратегии:\n sbackup all\n\n Выполнить копирование (ZIP):\n sbackup save\n\n Выполнить копирование (tar.gz):\n sbackup --format tar.gz save\n\n Выполнить копирование (7z с шифрованием):\n sbackup --format 7z save --password 123456\n\n Сохранить последние 5 копий:\n sbackup save --keep 5\n\n Автоматическое копирование (каждые 60 мин):\n sbackup watch --interval 60\n\n Автоматическое копирование + очистка:\n sbackup watch --interval 120 --keep 10\n\n Восстановление из копии:\n sbackup restore backup.7z F:/restored\n\n Китайский интерфейс:\n sbackup --lang zh_CN save\n\n Режим отладки:\n sbackup --debug save", + "cli.help.debug": "Включить режим отладки с подробным журналированием", + "cli.help.help": "Показать эту справку и выйти", + "cli.help.lang": "Выбрать язык интерфейса: zh_CN / en_US / fr_FR / es_ES / ru_RU / de_DE / ja_JP / pt_BR / ko_KR", + "cli.help.format": "Выбрать формат архивации: zip (по умолч.) / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.subcommands": "Выбрать команду для выполнения", + "cli.help.add": "Добавить новую стратегию резервного копирования", + "cli.help.add.source": "Путь к исходной папке: каталог для резервного копирования", + "cli.help.add.dest": "Путь к целевой папке: место хранения файлов резервных копий", + "cli.help.add.ignore": "Файлы или папки для исключения (через запятую, по умолч.: .git,__pycache__)", + "cli.help.add.format": "Формат архивации уровня записи (только для этой стратегии, по умолчанию глобальный): zip / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.rm": "Удалить стратегию резервного копирования", + "cli.help.rm.path": "Путь к исходной папке стратегии для удаления", + "cli.help.all": "Показать все стратегии резервного копирования", + "cli.help.save": "Выполнить все стратегии резервного копирования", + "cli.help.save.keep": "Сохранить последние N файлов копий, 0 — без очистки", + "cli.help.save.password": "Пароль шифрования (только для формата 7z)", + "cli.help.watch": "Выполнять резервное копирование периодически", + "cli.help.watch.interval": "Интервал копирования в минутах, по умолч. 60", + "cli.help.watch.keep": "Сохранить последние N файлов копий", + "cli.help.watch.password": "Пароль шифрования (только для формата 7z)", + "cli.help.restore": "Восстановить из файла резервной копии", + "cli.help.restore.file": "Путь к файлу резервной копии (.zip / .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .7z)", + "cli.help.restore.dir": "Целевой каталог для восстановления", + "cli.help.version": "Показать информацию о версии", + "cli.version": "\nSbackup v{version} — Copyright © 2026 xiatianxuan\nЛицензия GNU GPL v3.0 — https://www.gnu.org/licenses/gpl-3.0.html\n", + + "log.config.malformed": "Файл конфигурации %s повреждён, используются значения по умолчанию", + "log.config.reset": "Файл конфигурации %s повреждён, сброс", + "log.config.mkdir.error": "Невозможно создать каталог конфигурации %s : %s", + "log.config.write.error": "Невозможно записать файл конфигурации %s : %s", + "log.data.read": "Чтение файла данных : %s", + "log.data.create": "Файл данных не существует, создание : %s", + "log.data.load": "Загрузка существующего файла данных : %s", + "log.data.write": "Запись файла данных : %s", + "log.cleanup.delete": "Удалена старая резервная копия : %s", + "log.i18n.load.failed": "Не удалось загрузить локаль {path}: {error}", + "log.i18n.load.fallback_failed": "Не удалось загрузить локаль возврата {path}: {error}" +} \ No newline at end of file diff --git a/sbackup/locales/zh_CN.json b/sbackup/locales/zh_CN.json index 9e49d13..6692578 100644 --- a/sbackup/locales/zh_CN.json +++ b/sbackup/locales/zh_CN.json @@ -7,7 +7,7 @@ "warn.no.strategy.found": "警告:未找到 {path} 的备份策略。", "warn.source.missing": "警告:源文件夹不存在: {path}", "warn.json.decode.error": "警告:数据文件 {path} 格式错误,初始化为空字典。", - "warn.json.backup": "警告:已备份损坏的数据文件到 {path},", + "warn.json.backup": "警告:已备份损坏的数据文件到 {path}", "warn.json.renamed": "警告:已将损坏的数据文件重命名为 {path}。", "compress.success": "成功备份: {path} ({size:.2f} MB, {count} 个文件)", "compress.progress": "正在压缩", @@ -15,6 +15,13 @@ "err.permission": "权限不足:无法写入 '{path}'", "err.os": "系统错误:{error}", "err.unknown": "未知错误:{error}", + "err.unknown.format": "错误:无法识别的备份文件格式 {path}", + "err.argparse.invalid_choice": "无效的选择: ", + "err.argparse.choose_from": "可选值", + "err.argparse.invalid_float": "无效的浮点数值: ", + "err.argparse.invalid_int": "无效的整数值: ", + "err.argparse.unrecognized_args": "未识别的参数: ", + "err.argparse.required": "必需参数", "warn.zip.overwrite": "警告:将覆盖已有的备份文件 {path}", "warn.invalid.compresslevel": "警告:压缩级别 {level} 超出范围 (0-9),使用默认值 6", "cmd.save.uptodate": "所有备份策略均已是最新,无需备份。", @@ -24,5 +31,50 @@ "table.header.source": "源文件夹路径", "table.header.dest": "目标文件夹路径", "table.header.ignore": "忽略模式", - "table.cell.none": "无" -} \ No newline at end of file + "table.cell.none": "无", + "table.header.format": "格式", + "table.cell.default": "默认", + "restore.progress": "正在还原", + "restore.success": "还原成功: {path} ({count} 个文件)", + "cmd.watch.start": "定时备份已启动,间隔 {interval} 分钟(按 Ctrl+C 停止)", + + "cli.description": "Sbackup v{version} — 轻量级增量备份工具\n\nSbackup 帮助您轻松管理多文件夹的备份任务。它采用增量备份策略,\n仅对已修改的文件进行压缩,支持自定义忽略规则,并提供直观的进度反馈。\n\n可用命令:\n add 添加新的备份策略 (源目录 -> 目标目录)\n rm 删除现有的备份策略\n all 显示所有已配置的备份策略详情\n save 根据策略执行备份任务\n version 查看版本信息", + "cli.epilog": "示例:\n 添加备份策略:\n sbackup add F:/my_folder F:/backup -i node_modules,.git\n\n 添加策略并指定条目级格式:\n sbackup add F:/my_folder F:/backup --format tar.gz\n\n 删除备份策略:\n sbackup rm F:/my_folder\n\n 查看所有策略:\n sbackup all\n\n 执行备份 (ZIP):\n sbackup save\n\n 执行备份 (tar.gz):\n sbackup --format tar.gz save\n\n 执行备份 (7z 加密):\n sbackup --format 7z save --password 123456\n\n 保留最近 5 个备份:\n sbackup save --keep 5\n\n 定时备份 (每 60 分钟):\n sbackup watch --interval 60\n\n 定时备份 + 清理:\n sbackup watch --interval 120 --keep 10\n\n 还原备份:\n sbackup restore backup.7z F:/restored\n\n 英文界面:\n sbackup --lang en_US save\n\n 调试模式:\n sbackup --debug save", + "cli.help.debug": "开启调试模式,输出详细的运行日志和状态信息", + "cli.help.help": "显示此帮助信息并退出", + "cli.help.lang": "设置界面语言: zh_CN / en_US / fr_FR / es_ES / ru_RU / de_DE / ja_JP / pt_BR / ko_KR", + "cli.help.format": "设置打包格式: zip (默认) / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.subcommands": "选择要执行的命令", + "cli.help.add": "添加新的备份策略", + "cli.help.add.source": "源文件夹路径: 需要被备份的目录", + "cli.help.add.dest": "目标文件夹路径: 存放生成的备份文件的目录", + "cli.help.add.ignore": "需要忽略的文件或文件夹名称 (使用逗号分隔,默认: .git,__pycache__)", + "cli.help.add.format": "条目级打包格式(仅作用于该备份策略,不指定则使用全局默认): zip / tar / tar.gz / tar.bz2 / tar.xz / tar.zst / 7z", + "cli.help.rm": "删除备份策略", + "cli.help.rm.path": "需要删除备份策略的源文件夹路径", + "cli.help.all": "查看所有备份策略", + "cli.help.save": "执行所有备份策略", + "cli.help.save.keep": "保留最近 N 个备份文件,0 表示不清理", + "cli.help.save.password": "加密密码(仅 7z 格式支持)", + "cli.help.watch": "定时执行备份", + "cli.help.watch.interval": "备份间隔(分钟),默认 60", + "cli.help.watch.keep": "保留最近 N 个备份文件", + "cli.help.watch.password": "加密密码(仅 7z 格式支持)", + "cli.help.restore": "从备份文件还原", + "cli.help.restore.file": "备份文件路径 (.zip / .tar / .tar.gz / .tar.bz2 / .tar.xz / .tar.zst / .7z)", + "cli.help.restore.dir": "还原目标目录", + "cli.help.version": "查看版本信息", + "cli.version": "\nSbackup v{version} — Copyright © 2026 xiatianxuan\nLicensed under GNU GPL v3.0 — https://www.gnu.org/licenses/gpl-3.0.html\n", + + "log.config.malformed": "配置文件 %s 格式错误,使用默认配置", + "log.config.reset": "配置文件 %s 格式错误,重置为空", + "log.config.mkdir.error": "无法创建配置目录 %s: %s", + "log.config.write.error": "无法写入配置文件 %s: %s", + "log.data.read": "读取数据文件: %s", + "log.data.create": "数据文件不存在,创建新文件: %s", + "log.data.load": "加载现有数据文件: %s", + "log.data.write": "写入数据文件: %s", + "log.cleanup.delete": "已删除旧备份: %s", + "log.i18n.load.failed": "无法加载语言包 {path}: {error}", + "log.i18n.load.fallback_failed": "无法加载回退语言包 {path}: {error}" +} diff --git a/test_compression.py b/test_compression.py deleted file mode 100644 index 6e0da54..0000000 --- a/test_compression.py +++ /dev/null @@ -1,11 +0,0 @@ -import unittest -from sbackup._compression import load_config - -class TestCompression(unittest.TestCase): - def test_load_config(self): - config = load_config() - self.assertEqual(config.compression_level, 5) - self.assertEqual(config.auto_save_interval, 300) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/__init__.py b/tests/__init__.py index 56f3b36..d3f5a12 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ - + diff --git a/tests/sbackup/__init__.py b/tests/sbackup/__init__.py index 56f3b36..d3f5a12 100644 --- a/tests/sbackup/__init__.py +++ b/tests/sbackup/__init__.py @@ -1 +1 @@ - + diff --git a/tests/sbackup/test_auto_save.py b/tests/sbackup/test_auto_save.py index 08b6e8e..ba0d503 100644 --- a/tests/sbackup/test_auto_save.py +++ b/tests/sbackup/test_auto_save.py @@ -20,14 +20,14 @@ class TestAutoSave(unittest.TestCase): """ # 创建临时目录 self.test_dir = tempfile.mkdtemp() - + # 保存原始工作目录并切换到测试目录,以便测试相对路径逻辑 self.original_cwd = os.getcwd() os.chdir(self.test_dir) # 定义测试使用的数据文件 self.data_file = os.path.join(self.test_dir, "test_sbackup.json") - + # 初始化 BackupManager 实例 self.manager = BackupManager(self.data_file) @@ -50,15 +50,19 @@ class TestAutoSave(unittest.TestCase): """测试添加备份策略""" # 添加策略 result = self.manager.add_folder(self.source_folder, self.target_folder, ".git") - + self.assertTrue(result) # 验证数据文件 - self.assertTrue(os.path.exists(self.data_file), f"数据文件 {self.data_file} 不存在") + self.assertTrue( + os.path.exists(self.data_file), f"数据文件 {self.data_file} 不存在" + ) with open(self.data_file, "r") as f: data = json.load(f) - self.assertIn(os.path.abspath(self.source_folder), data, f"数据文件内容: {data}") + self.assertIn( + os.path.abspath(self.source_folder), data, f"数据文件内容: {data}" + ) def test_rm_folder(self): """测试删除备份策略""" @@ -99,8 +103,7 @@ class TestAutoSave(unittest.TestCase): self.assertTrue(hasattr(self.manager, "save_folder")) # 验证 save_folder 指向 execute_backups 的底层函数 self.assertIs( - self.manager.save_folder.__func__, - self.manager.execute_backups.__func__ + self.manager.save_folder.__func__, self.manager.execute_backups.__func__ ) def test_add_folder_skip_patterns_stripped(self): @@ -119,7 +122,6 @@ class TestAutoSave(unittest.TestCase): self.assertTrue(result1) result2 = self.manager.add_folder(self.source_folder, self.target_folder) self.assertFalse(result2) - abs_source = os.path.abspath(self.source_folder) self.assertEqual(len(self.manager.data), 1) def test_rm_nonexistent_folder(self): @@ -140,7 +142,7 @@ class TestAutoSave(unittest.TestCase): def test_save_missing_source(self): """测试源文件夹不存在时的 save 行为""" # 手动构造一条源文件夹不存在的记录 - self.manager.data["/nonexistent/source"] = [0.0, self.target_folder, []] + self.manager.data["/nonexistent/source"] = [0.0, self.target_folder, [], ""] self.manager.save() # save_folder 应打印警告并跳过,不抛出异常 self.manager.save_folder() @@ -159,6 +161,300 @@ class TestAutoSave(unittest.TestCase): abs_source = os.path.abspath(self.source_folder) self.assertEqual(result[abs_source], os.path.abspath(self.target_folder)) + def test_list_folder_table_with_data(self): + """测试有策略时表格渲染""" + self.manager.add_folder(self.source_folder, self.target_folder, ".git") + text = self.manager.list_folder_table() + self.assertIn(os.path.abspath(self.source_folder), text) + self.assertIn(os.path.abspath(self.target_folder), text) + self.assertIn(".git", text) + + def test_load_corrupted_file_rename(self): + """测试损坏文件备份失败时重命名""" + # 写入损坏的 JSON + with open(self.data_file, "w") as f: + f.write("{corrupted!!!") + # 模拟 shutil.copy2 失败 + + original_copy2 = shutil.copy2 + + def failing_copy2(*args, **kwargs): + raise OSError("copy failed") + + shutil.copy2 = failing_copy2 + try: + mgr = BackupManager(self.data_file) + # 应重命名损坏文件 + self.assertTrue(os.path.exists(self.data_file + ".corrupted")) + self.assertFalse(os.path.exists(self.data_file)) + self.assertEqual(mgr.data, {}) + finally: + shutil.copy2 = original_copy2 + + def test_get_entry_and_set_entry(self): + """测试 _get_entry 和 _set_entry 辅助方法""" + from sbackup.auto_save import BackupEntry + + self.manager.add_folder(self.source_folder, self.target_folder, ".git") + abs_source = os.path.abspath(self.source_folder) + + entry = self.manager._get_entry(abs_source) + assert entry is not None, "entry should not be None" + self.assertEqual(entry.target, os.path.abspath(self.target_folder)) + self.assertEqual(entry.skip_patterns, [".git"]) + + # 测试不存在的 key + self.assertIsNone(self.manager._get_entry("/nonexistent")) + + # 测试 _set_entry + new_entry = BackupEntry( + mtime=999.0, target="/new/target", skip_patterns=["*.log"] + ) + self.manager._set_entry(abs_source, new_entry) + retrieved = self.manager._get_entry(abs_source) + assert retrieved is not None, "retrieved entry should not be None" + self.assertEqual(retrieved.mtime, 999.0) + self.assertEqual(retrieved.target, "/new/target") + + def test_execute_backups_skip_uptodate(self): + """测试所有策略已最新时输出提示""" + self.manager.add_folder(self.source_folder, self.target_folder, "") + # 不修改文件,直接执行备份 + self.manager.execute_backups() + # 应输出"已是最新"(通过检查 data 中 mtime 未变来验证) + abs_source = os.path.abspath(self.source_folder) + self.assertEqual(self.manager.data[abs_source][0], os.stat(abs_source).st_mtime) + + def test_execute_backups_oserror_on_stat(self): + """测试 os.stat 失败时跳过该策略""" + self.manager.add_folder(self.source_folder, self.target_folder, "") + abs_source = os.path.abspath(self.source_folder) + original_mtime = self.manager.data[abs_source][0] + + # 修改文件触发备份条件 + time.sleep(0.1) + (Path(self.source_folder) / "f.txt").write_text("x") + + # 模拟 os.stat 失败 + original_stat = os.stat + + def failing_stat(path, *args, **kwargs): + if path == abs_source: + raise OSError("stat failed") + return original_stat(path, *args, **kwargs) + + os.stat = failing_stat + try: + self.manager.execute_backups() + # mtime 不应被更新 + self.assertEqual(self.manager.data[abs_source][0], original_mtime) + finally: + os.stat = original_stat + + def test_load_corrupted_file_backup_success(self): + """测试损坏文件成功备份到 .bak""" + with open(self.data_file, "w") as f: + f.write("{corrupted!!!") + mgr = BackupManager(self.data_file) + self.assertTrue(os.path.exists(self.data_file + ".bak")) + self.assertEqual(mgr.data, {}) + + def test_add_folder_oserror_on_stat(self): + """测试 add_folder 时 os.stat 失败""" + # 创建一个无权限的目录场景比较难,直接 mock os.stat + original_stat = os.stat + + def failing_stat(path, *args, **kwargs): + raise OSError("stat failed") + + os.stat = failing_stat + try: + result = self.manager.add_folder(self.source_folder, self.target_folder) + self.assertFalse(result) + finally: + os.stat = original_stat + + def test_backup_history(self): + """测试备份历史记录""" + self.manager.add_folder(self.source_folder, self.target_folder, "") + abs_source = os.path.abspath(self.source_folder) + + time.sleep(0.1) + (Path(self.source_folder) / "f.txt").write_text("x") + self.manager.execute_backups() + + history = self.manager.get_history() + self.assertEqual(len(history), 1) + self.assertEqual(history[0]["source"], abs_source) + self.assertIn("time", history[0]) + self.assertIn("size_mb", history[0]) + self.assertIn("files_count", history[0]) + + def test_backup_history_empty(self): + """测试无备份时历史为空""" + history = self.manager.get_history() + self.assertEqual(history, []) + + def test_cleanup_old_backups(self): + """测试清理旧备份文件""" + # 创建多个假备份文件 + target = Path(self.target_folder) + for i in range(5): + (target / f"backup_{i}.zip").write_text(f"content{i}") + import time + + time.sleep(0.05) + + BackupManager._cleanup_old_backups(self.target_folder, keep=2) + remaining = list(target.glob("*.zip")) + self.assertLessEqual(len(remaining), 2) + + def test_cleanup_old_backups_none(self): + """测试 keep=0 时不清理""" + target = Path(self.target_folder) + (target / "backup.zip").write_text("data") + BackupManager._cleanup_old_backups(self.target_folder, keep=0) + self.assertTrue((target / "backup.zip").exists()) + + def test_cleanup_old_backups_invalid_dir(self): + """测试清理不存在的目录""" + BackupManager._cleanup_old_backups("/nonexistent", keep=2) + # 不应抛出异常 + + def test_execute_backups_with_keep(self): + """测试执行备份时保留 N 个文件""" + self.manager.add_folder(self.source_folder, self.target_folder, "") + + time.sleep(0.1) + (Path(self.source_folder) / "f1.txt").write_text("x") + self.manager.execute_backups(keep=3) + + history = self.manager.get_history() + self.assertGreaterEqual(len(history), 1) + + def test_list_folder_table_skips_history(self): + """测试 list_folder_table 跳过 _history 键""" + self.manager.add_folder(self.source_folder, self.target_folder, ".git") + # 手动添加 _history 数据 + self.manager.data["_history"] = [ + {"time": "2026-01-01", "source": "/x", "size_mb": 1.0, "files_count": 1} + ] + text = self.manager.list_folder_table() + # 不应包含 _history 相关文本 + self.assertNotIn("_history", text) + # 应正常显示备份策略 + self.assertIn(os.path.abspath(self.source_folder), text) + + def test_execute_backups_skips_history(self): + """测试 execute_backups 跳过 _history 键""" + self.manager.add_folder(self.source_folder, self.target_folder, "") + # 手动添加 _history 数据 + self.manager.data["_history"] = [ + {"time": "2026-01-01", "source": "/x", "size_mb": 1.0, "files_count": 1} + ] + self.manager.save() + + time.sleep(0.1) + (Path(self.source_folder) / "h.txt").write_text("x") + # 不应因 _history 抛出异常 + self.manager.execute_backups() + history = self.manager.get_history() + self.assertGreaterEqual(len(history), 1) + + def test_add_folder_with_format(self): + """测试添加策略时指定条目级格式""" + result = self.manager.add_folder( + self.source_folder, self.target_folder, ".git", "TAR_GZ" + ) + self.assertTrue(result) + abs_source = os.path.abspath(self.source_folder) + entry = self.manager._get_entry(abs_source) + assert entry is not None, "entry should not be None" + self.assertEqual(entry.compression_format, "TAR_GZ") + + def test_add_folder_without_format(self): + """测试添加策略时不指定格式则 compression_format 为空""" + result = self.manager.add_folder(self.source_folder, self.target_folder, ".git") + self.assertTrue(result) + abs_source = os.path.abspath(self.source_folder) + entry = self.manager._get_entry(abs_source) + assert entry is not None, "entry should not be None" + self.assertEqual(entry.compression_format, "") + + def test_entry_format_backward_compatibility(self): + """测试 from_list 向后兼容旧格式(3元素列表)""" + from sbackup.auto_save import BackupEntry + + old_data = [1719235200.0, "/path/to/target", [".git"]] + entry = BackupEntry.from_list(old_data) + self.assertEqual(entry.mtime, 1719235200.0) + self.assertEqual(entry.target, "/path/to/target") + self.assertEqual(entry.skip_patterns, [".git"]) + self.assertEqual(entry.compression_format, "") + + def test_entry_format_new_format(self): + """测试 from_list 正确读取新格式(4元素列表)""" + from sbackup.auto_save import BackupEntry + + new_data = [1719235200.0, "/path/to/target", [".git"], "TAR_GZ"] + entry = BackupEntry.from_list(new_data) + self.assertEqual(entry.compression_format, "TAR_GZ") + + def test_to_list_includes_format(self): + """测试 to_list 输出包含 compression_format""" + from sbackup.auto_save import BackupEntry + + entry = BackupEntry( + mtime=1.0, target="/t", skip_patterns=[], compression_format="7Z" + ) + self.assertEqual(entry.to_list(), [1.0, "/t", [], "7Z"]) + + def test_execute_backups_entry_format_overrides_global(self): + """测试条目级格式优先于全局格式""" + # 添加策略,指定条目级格式为 TAR_GZ + self.manager.add_folder(self.source_folder, self.target_folder, "", "TAR_GZ") + + time.sleep(0.1) + (Path(self.source_folder) / "f_fmt.txt").write_text("format test") + + self.manager.execute_backups() + + # 检查生成的备份文件是 .tar.gz + target_path = Path(self.target_folder) + gz_files = list(target_path.glob("*.tar.gz")) + self.assertGreaterEqual(len(gz_files), 1, "应生成 .tar.gz 备份文件") + + def test_execute_backups_global_format_when_entry_empty(self): + """测试条目级格式为空时使用全局格式""" + # 添加策略,不指定条目级格式 + self.manager.add_folder(self.source_folder, self.target_folder, "") + + time.sleep(0.1) + (Path(self.source_folder) / "f_global.txt").write_text("global test") + + # 全局配置为 ZIP(默认) + self.manager.execute_backups() + + target_path = Path(self.target_folder) + zip_files = list(target_path.glob("*.zip")) + self.assertGreaterEqual(len(zip_files), 1, "应生成 .zip 备份文件") + + def test_list_folder_table_shows_format(self): + """测试表格显示格式列""" + self.manager.add_folder( + self.source_folder, self.target_folder, ".git", "TAR_GZ" + ) + text = self.manager.list_folder_table() + # 格式列应包含 TAR_GZ + self.assertIn("TAR_GZ", text) + + def test_list_folder_table_shows_default_when_no_format(self): + """测试表格中无条目级格式时显示默认""" + self.manager.add_folder(self.source_folder, self.target_folder, ".git") + text = self.manager.list_folder_table() + # 应包含"默认"文本(中文环境) + self.assertIn(t("table.cell.default"), text) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/sbackup/test_compression.py b/tests/sbackup/test_compression.py index 6dd80c3..9e1c45f 100644 --- a/tests/sbackup/test_compression.py +++ b/tests/sbackup/test_compression.py @@ -1,20 +1,56 @@ """ 单元测试 for sbackup.compression 模块 """ + import unittest import os import shutil +import tarfile import zipfile from pathlib import Path from unittest.mock import patch from sbackup.config import Config -from sbackup.compression import ZipfileCompression +from sbackup.compression import ( + ZipfileCompression, + TarfileCompression, + ZstdCompression, + SevenZipCompression, + create_compressor, + restore_backup, +) from sbackup.i18n import t +_TEST_ARTIFACTS = [ + "test.zip", + "test.tar", + "test.tar.gz", + "test.tar.bz2", + "test.tar.xz", + "test.tar.zst", + "test.7z", + "test_output.zip", + "test_data.zip", + "test_data.tar.gz", + "test_data.tar.zst", + "test.unknown", + "test_data", + "test_restore", +] + + +def tearDownModule(): + """模块级清理:删除所有测试残留文件""" + for name in _TEST_ARTIFACTS: + path = Path(name) + if path.is_dir(): + shutil.rmtree(path, ignore_errors=True) + elif path.exists(): + path.unlink(missing_ok=True) + + class TestCompression(unittest.TestCase): def setUp(self): - # 创建测试文件夹 self.test_dir = "test_data" os.makedirs(self.test_dir, exist_ok=True) (Path(self.test_dir) / "file1.txt").write_text("test content 1") @@ -26,39 +62,39 @@ class TestCompression(unittest.TestCase): os.remove(self.zip_path) def tearDown(self): - # 清理测试文件夹 if os.path.exists(self.test_dir): shutil.rmtree(self.test_dir) if os.path.exists(self.zip_path): os.remove(self.zip_path) + for f in [ + "test.tar.gz", + "test.tar.bz2", + "test.tar.xz", + "test_output.zip", + "test_data.zip", + "test_data.tar.gz", + ]: + if os.path.exists(f): + os.remove(f) def test_zip_folder_basic(self): - """测试基本压缩功能""" - config = Config( - folder_path=self.test_dir, - zipfile_path=self.zip_path, - ) + """测试基本 ZIP 压缩功能""" + config = Config(folder_path=self.test_dir, zipfile_path=self.zip_path) compressor = ZipfileCompression(config) - compressor.zip_folder() - + compressor.compress() self.assertTrue(os.path.exists(self.zip_path)) self.assertGreater(os.path.getsize(self.zip_path), 0) def test_zip_internal_path_structure(self): - """测试 ZIP 内部路径结构正确(以文件夹名开头,而非绝对路径)""" - config = Config( - folder_path=self.test_dir, - zipfile_path=self.zip_path, - ) + """测试 ZIP 内部路径结构正确""" + config = Config(folder_path=self.test_dir, zipfile_path=self.zip_path) compressor = ZipfileCompression(config) - compressor.zip_folder() - + compressor.compress() with zipfile.ZipFile(self.zip_path, "r") as zf: - namelist = zf.namelist() - for name in namelist: + for name in zf.namelist(): self.assertTrue( name.startswith("test_data/"), - f"ZIP 内部路径 '{name}' 应以 'test_data/' 开头" + f"ZIP 内部路径 '{name}' 应以 'test_data/' 开头", ) def test_zip_respects_skip_patterns(self): @@ -70,23 +106,18 @@ class TestCompression(unittest.TestCase): skip_patterns=[".gitignore"], ) compressor = ZipfileCompression(config) - result = compressor.zip_folder() - + result = compressor.compress() self.assertTrue(result["success"]) with zipfile.ZipFile(self.zip_path, "r") as zf: - namelist = zf.namelist() - self.assertNotIn("test_data/.gitignore", namelist) + self.assertNotIn("test_data/.gitignore", zf.namelist()) @patch("builtins.print") def test_compresslevel_out_of_range(self, mock_print): """测试压缩级别超出范围时回退到默认值""" config = Config( - folder_path=self.test_dir, - zipfile_path=self.zip_path, - compression_level=99, + folder_path=self.test_dir, zipfile_path=self.zip_path, compression_level=99 ) compressor = ZipfileCompression(config) - # compresslevel 应回退到 6 self.assertEqual(compressor.compression_level, 6) def test_compresslevel_none_for_stored(self): @@ -103,18 +134,576 @@ class TestCompression(unittest.TestCase): @patch("builtins.print") def test_zip_overwrite_warning(self, mock_print): """测试 ZIP 文件已存在时输出覆盖警告""" - config = Config( - folder_path=self.test_dir, - zipfile_path=self.zip_path, - ) + config = Config(folder_path=self.test_dir, zipfile_path=self.zip_path) compressor = ZipfileCompression(config) - # 第一次压缩 - compressor.zip_folder() - # 第二次压缩(文件已存在) - compressor.zip_folder() + compressor.compress() + compressor.compress() printed = " ".join(str(call) for call in mock_print.call_args_list) self.assertIn(t("warn.zip.overwrite").split("{")[0], printed) + def test_unknown_compression_algorithm_fallback(self): + """测试未知压缩算法回退到 ZIP_DEFLATED""" + config = Config( + folder_path=self.test_dir, + zipfile_path=self.zip_path, + compression_algorithm="UNKNOWN_ALGO", + ) + compressor = ZipfileCompression(config) + self.assertEqual(compressor.compression_algorithm, zipfile.ZIP_DEFLATED) + + def test_zip_folder_with_dir_as_zipfile_path(self): + """测试 zipfile_path 为目录时自动拼接文件名""" + config = Config(folder_path=self.test_dir, zipfile_path=self.test_dir) + compressor = ZipfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + expected = os.path.join(self.test_dir, "test_data.zip") + self.assertTrue(os.path.exists(expected)) + os.remove(expected) + + def test_zip_folder_without_zip_extension(self): + """测试 zipfile_path 无 .zip 后缀时自动添加""" + config = Config(folder_path=self.test_dir, zipfile_path="test_output") + compressor = ZipfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + self.assertTrue(os.path.exists("test_output.zip")) + os.remove("test_output.zip") + + def test_zip_folder_invalid_source(self): + """测试压缩不存在的文件夹""" + config = Config(folder_path="/nonexistent/folder", zipfile_path=self.zip_path) + compressor = ZipfileCompression(config) + result = compressor.compress() + self.assertFalse(result["success"]) + + @patch("builtins.print") + def test_zip_folder_permission_error(self, mock_print): + """测试压缩时权限不足""" + config = Config(folder_path=self.test_dir, zipfile_path="/root/forbidden.zip") + compressor = ZipfileCompression(config) + result = compressor.compress() + self.assertFalse(result["success"]) + + @patch("builtins.print") + def test_zip_folder_os_error(self, mock_print): + """测试压缩时系统错误""" + config = Config(folder_path=self.test_dir, zipfile_path=self.zip_path) + compressor = ZipfileCompression(config) + with patch("zipfile.ZipFile", side_effect=OSError("disk error")): + result = compressor.compress() + self.assertFalse(result["success"]) + + def test_compresslevel_none_for_lzma(self): + """测试 ZIP_LZMA 算法不传递 compresslevel""" + config = Config( + folder_path=self.test_dir, + zipfile_path=self.zip_path, + compression_algorithm="ZIP_LZMA", + compression_level=6, + ) + compressor = ZipfileCompression(config) + self.assertIsNone(compressor.compression_level) + + def test_zip_bzip2_algorithm(self): + """测试 ZIP_BZIP2 压缩算法""" + config = Config( + folder_path=self.test_dir, + zipfile_path=self.zip_path, + compression_algorithm="ZIP_BZIP2", + compression_level=6, + ) + compressor = ZipfileCompression(config) + self.assertEqual(compressor.compression_algorithm, zipfile.ZIP_BZIP2) + self.assertEqual(compressor.compression_level, 6) + + def test_zip_folder_without_zipfile_path(self): + """测试不指定 zipfile_path 时自动生成""" + config = Config(folder_path=self.test_dir, zipfile_path=None) + compressor = ZipfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + expected = os.path.join( + os.path.dirname(os.path.abspath(self.test_dir)), "test_data.zip" + ) + self.assertTrue(os.path.exists(expected)) + os.remove(expected) + + @patch("builtins.print") + def test_zip_folder_unknown_exception(self, mock_print): + """测试压缩时未知异常""" + config = Config(folder_path=self.test_dir, zipfile_path=self.zip_path) + compressor = ZipfileCompression(config) + with patch("zipfile.ZipFile", side_effect=RuntimeError("unexpected")): + result = compressor.compress() + self.assertFalse(result["success"]) + + +class TestTarCompression(unittest.TestCase): + def setUp(self): + self.test_dir = "test_data" + os.makedirs(self.test_dir, exist_ok=True) + (Path(self.test_dir) / "file1.txt").write_text("test content 1") + (Path(self.test_dir) / "subdir").mkdir() + (Path(self.test_dir) / "subdir" / "file2.txt").write_text("test content 2") + + def tearDown(self): + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + for f in ["test.tar.gz", "test.tar.bz2", "test.tar.xz", "test_data.tar.gz"]: + if os.path.exists(f): + os.remove(f) + + def test_tar_gz_basic(self): + """测试 tar.gz 基本压缩""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.gz", + compression_format="TAR_GZ", + ) + compressor = TarfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + self.assertTrue(os.path.exists("test.tar.gz")) + self.assertGreater(os.path.getsize("test.tar.gz"), 0) + + def test_tar_bz2_basic(self): + """测试 tar.bz2 基本压缩""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.bz2", + compression_format="TAR_BZ2", + ) + compressor = TarfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + self.assertTrue(os.path.exists("test.tar.bz2")) + + def test_tar_xz_basic(self): + """测试 tar.xz 基本压缩""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.xz", + compression_format="TAR_XZ", + ) + compressor = TarfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + self.assertTrue(os.path.exists("test.tar.xz")) + + def test_tar_internal_path_structure(self): + """测试 TAR 内部路径结构正确""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.gz", + compression_format="TAR_GZ", + ) + compressor = TarfileCompression(config) + compressor.compress() + with tarfile.open("test.tar.gz", "r:gz") as tarf: + names = tarf.getnames() + for name in names: + self.assertTrue( + name.startswith("test_data/"), + f"TAR 内部路径 '{name}' 应以 'test_data/' 开头", + ) + + def test_tar_respects_skip_patterns(self): + """测试 TAR 压缩时正确忽略匹配的文件""" + (Path(self.test_dir) / ".gitignore").write_text("ignore") + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.gz", + compression_format="TAR_GZ", + skip_patterns=[".gitignore"], + ) + compressor = TarfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + with tarfile.open("test.tar.gz", "r:gz") as tarf: + self.assertNotIn("test_data/.gitignore", tarf.getnames()) + + def test_tar_without_zipfile_path(self): + """测试不指定路径时自动生成 tar.gz""" + config = Config( + folder_path=self.test_dir, zipfile_path=None, compression_format="TAR_GZ" + ) + compressor = TarfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + expected = os.path.join( + os.path.dirname(os.path.abspath(self.test_dir)), "test_data.tar.gz" + ) + self.assertTrue(os.path.exists(expected)) + os.remove(expected) + + def test_tar_compresslevel_out_of_range(self): + """测试 TAR 压缩级别超出范围""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.gz", + compression_format="TAR_GZ", + compression_level=99, + ) + compressor = TarfileCompression(config) + self.assertEqual(compressor.compression_level, 6) + + def test_tar_invalid_source(self): + """测试 TAR 压缩不存在的文件夹""" + config = Config( + folder_path="/nonexistent/folder", + zipfile_path="test.tar.gz", + compression_format="TAR_GZ", + ) + compressor = TarfileCompression(config) + result = compressor.compress() + self.assertFalse(result["success"]) + + def test_create_compressor_zip(self): + """测试工厂函数创建 ZIP 压缩器""" + config = Config(folder_path=self.test_dir, compression_format="ZIP") + compressor = create_compressor(config) + self.assertIsInstance(compressor, ZipfileCompression) + + def test_create_compressor_tar_gz(self): + """测试工厂函数创建 TAR_GZ 压缩器""" + config = Config(folder_path=self.test_dir, compression_format="TAR_GZ") + compressor = create_compressor(config) + self.assertIsInstance(compressor, TarfileCompression) + + def test_create_compressor_tar_bz2(self): + """测试工厂函数创建 TAR_BZ2 压缩器""" + config = Config(folder_path=self.test_dir, compression_format="TAR_BZ2") + compressor = create_compressor(config) + self.assertIsInstance(compressor, TarfileCompression) + + def test_create_compressor_tar_xz(self): + """测试工厂函数创建 TAR_XZ 压缩器""" + config = Config(folder_path=self.test_dir, compression_format="TAR_XZ") + compressor = create_compressor(config) + self.assertIsInstance(compressor, TarfileCompression) + + +class TestRestore(unittest.TestCase): + def setUp(self): + self.test_dir = "test_data" + os.makedirs(self.test_dir, exist_ok=True) + (Path(self.test_dir) / "file1.txt").write_text("test content 1") + (Path(self.test_dir) / "subdir").mkdir() + (Path(self.test_dir) / "subdir" / "file2.txt").write_text("test content 2") + self.restore_dir = "test_restore" + + def tearDown(self): + for d in [self.test_dir, self.restore_dir]: + if os.path.exists(d): + shutil.rmtree(d) + for f in ["test.zip", "test.tar.gz"]: + if os.path.exists(f): + os.remove(f) + + def test_restore_zip(self): + """测试从 ZIP 还原""" + config = Config(folder_path=self.test_dir, zipfile_path="test.zip") + ZipfileCompression(config).compress() + result = restore_backup("test.zip", self.restore_dir) + self.assertTrue(result["success"]) + self.assertTrue( + os.path.exists(os.path.join(self.restore_dir, "test_data", "file1.txt")) + ) + + def test_restore_tar_gz(self): + """测试从 tar.gz 还原""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.gz", + compression_format="TAR_GZ", + ) + TarfileCompression(config).compress() + result = restore_backup("test.tar.gz", self.restore_dir) + self.assertTrue(result["success"]) + self.assertTrue( + os.path.exists(os.path.join(self.restore_dir, "test_data", "file1.txt")) + ) + + def test_restore_nonexistent_file(self): + """测试还原不存在的文件""" + result = restore_backup("/nonexistent/backup.zip", self.restore_dir) + self.assertFalse(result["success"]) + + def test_restore_unknown_format(self): + """测试还原未知格式""" + Path("test.unknown").write_text("data") + result = restore_backup("test.unknown", self.restore_dir) + self.assertFalse(result["success"]) + os.remove("test.unknown") + + def test_restore_7z(self): + """测试从 7z 还原""" + config = Config( + folder_path=self.test_dir, zipfile_path="test.7z", compression_format="7Z" + ) + SevenZipCompression(config).compress() + result = restore_backup("test.7z", self.restore_dir) + self.assertTrue(result["success"]) + self.assertTrue( + os.path.exists(os.path.join(self.restore_dir, "test_data", "file1.txt")) + ) + os.remove("test.7z") + + def test_restore_tar_zst(self): + """测试从 tar.zst 还原""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.zst", + compression_format="TAR_ZST", + ) + ZstdCompression(config).compress() + result = restore_backup("test.tar.zst", self.restore_dir) + self.assertTrue(result["success"]) + self.assertTrue( + os.path.exists(os.path.join(self.restore_dir, "test_data", "file1.txt")) + ) + os.remove("test.tar.zst") + + +class TestPathMatching(unittest.TestCase): + def setUp(self): + self.test_dir = "test_data" + os.makedirs(self.test_dir, exist_ok=True) + (Path(self.test_dir) / "file1.txt").write_text("a") + (Path(self.test_dir) / "subdir").mkdir() + (Path(self.test_dir) / "subdir" / "file2.log").write_text("b") + (Path(self.test_dir) / "subdir" / "file3.txt").write_text("c") + + def tearDown(self): + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + for f in ["test.zip"]: + if os.path.exists(f): + os.remove(f) + + def test_path_pattern_matching(self): + """测试路径级忽略模式 subdir/*.log""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.zip", + skip_patterns=["subdir/*.log"], + ) + compressor = ZipfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + with zipfile.ZipFile("test.zip", "r") as zf: + names = zf.namelist() + self.assertIn("test_data/file1.txt", names) + self.assertIn("test_data/subdir/file3.txt", names) + self.assertNotIn("test_data/subdir/file2.log", names) + + def test_basename_pattern_still_works(self): + """测试基础文件名模式仍然有效""" + config = Config( + folder_path=self.test_dir, zipfile_path="test.zip", skip_patterns=["*.log"] + ) + compressor = ZipfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + with zipfile.ZipFile("test.zip", "r") as zf: + names = zf.namelist() + self.assertNotIn("test_data/subdir/file2.log", names) + + +class TestTarNoCompression(unittest.TestCase): + def setUp(self): + self.test_dir = "test_data" + os.makedirs(self.test_dir, exist_ok=True) + (Path(self.test_dir) / "file1.txt").write_text("test") + + def tearDown(self): + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + for f in ["test.tar"]: + if os.path.exists(f): + os.remove(f) + + def test_tar_no_compression(self): + """测试不压缩的 tar 格式""" + config = Config( + folder_path=self.test_dir, zipfile_path="test.tar", compression_format="TAR" + ) + compressor = TarfileCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + self.assertTrue(os.path.exists("test.tar")) + + +class TestZstdCompression(unittest.TestCase): + def setUp(self): + self.test_dir = "test_data" + os.makedirs(self.test_dir, exist_ok=True) + (Path(self.test_dir) / "file1.txt").write_text("test content") + + def tearDown(self): + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + for f in ["test.tar.zst"]: + if os.path.exists(f): + os.remove(f) + + def test_zstd_basic(self): + """测试 tar.zst 基本压缩""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.zst", + compression_format="TAR_ZST", + ) + compressor = ZstdCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + self.assertTrue(os.path.exists("test.tar.zst")) + + def test_zstd_compresslevel_out_of_range(self): + """测试 zstd 压缩级别超出范围""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.zst", + compression_format="TAR_ZST", + compression_level=99, + ) + compressor = ZstdCompression(config) + self.assertEqual(compressor.compression_level, 3) + + def test_create_compressor_zstd(self): + """测试工厂函数创建 Zstd 压缩器""" + config = Config(folder_path=self.test_dir, compression_format="TAR_ZST") + compressor = create_compressor(config) + self.assertIsInstance(compressor, ZstdCompression) + + def test_zstd_invalid_source(self): + """测试 zstd 压缩不存在的文件夹""" + config = Config( + folder_path="/nonexistent", + zipfile_path="test.tar.zst", + compression_format="TAR_ZST", + ) + compressor = ZstdCompression(config) + result = compressor.compress() + self.assertFalse(result["success"]) + + @patch("builtins.print") + def test_zstd_permission_error(self, mock_print): + """测试 zstd 压缩权限不足""" + config = Config( + folder_path=self.test_dir, + zipfile_path="/root/test.tar.zst", + compression_format="TAR_ZST", + ) + compressor = ZstdCompression(config) + result = compressor.compress() + self.assertFalse(result["success"]) + + @patch("builtins.print") + def test_zstd_os_error(self, mock_print): + """测试 zstd 压缩系统错误""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.tar.zst", + compression_format="TAR_ZST", + ) + compressor = ZstdCompression(config) + with patch("zstandard.ZstdCompressor", side_effect=OSError("error")): + result = compressor.compress() + self.assertFalse(result["success"]) + + +class TestSevenZipCompression(unittest.TestCase): + def setUp(self): + self.test_dir = "test_data" + os.makedirs(self.test_dir, exist_ok=True) + (Path(self.test_dir) / "file1.txt").write_text("test content") + + def tearDown(self): + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir) + for f in ["test.7z"]: + if os.path.exists(f): + os.remove(f) + + def test_7z_basic(self): + """测试 7z 基本压缩""" + config = Config( + folder_path=self.test_dir, zipfile_path="test.7z", compression_format="7Z" + ) + compressor = SevenZipCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + self.assertTrue(os.path.exists("test.7z")) + + def test_7z_compresslevel_out_of_range(self): + """测试 7z 压缩级别超出范围""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.7z", + compression_format="7Z", + compression_level=99, + ) + compressor = SevenZipCompression(config) + self.assertEqual(compressor.compression_level, 6) + + def test_create_compressor_7z(self): + """测试工厂函数创建 7z 压缩器""" + config = Config(folder_path=self.test_dir, compression_format="7Z") + compressor = create_compressor(config) + self.assertIsInstance(compressor, SevenZipCompression) + + def test_7z_invalid_source(self): + """测试 7z 压缩不存在的文件夹""" + config = Config( + folder_path="/nonexistent", zipfile_path="test.7z", compression_format="7Z" + ) + compressor = SevenZipCompression(config) + result = compressor.compress() + self.assertFalse(result["success"]) + + @patch("builtins.print") + def test_7z_permission_error(self, mock_print): + """测试 7z 压缩权限不足""" + config = Config( + folder_path=self.test_dir, + zipfile_path="/root/test.7z", + compression_format="7Z", + ) + compressor = SevenZipCompression(config) + result = compressor.compress() + self.assertFalse(result["success"]) + + @patch("builtins.print") + def test_7z_os_error(self, mock_print): + """测试 7z 压缩系统错误""" + config = Config( + folder_path=self.test_dir, zipfile_path="test.7z", compression_format="7Z" + ) + compressor = SevenZipCompression(config) + with patch("py7zr.SevenZipFile", side_effect=OSError("error")): + result = compressor.compress() + self.assertFalse(result["success"]) + + def test_7z_with_password(self): + """测试 7z 加密压缩""" + config = Config( + folder_path=self.test_dir, + zipfile_path="test.7z", + compression_format="7Z", + password="secret123", + ) + compressor = SevenZipCompression(config) + result = compressor.compress() + self.assertTrue(result["success"]) + self.assertTrue(os.path.exists("test.7z")) + import py7zr + + with py7zr.SevenZipFile("test.7z", "r", password="secret123") as szf: + names = szf.getnames() + self.assertGreater(len(names), 0) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/sbackup/test_config.py b/tests/sbackup/test_config.py index 267fa73..e5fb9b6 100644 --- a/tests/sbackup/test_config.py +++ b/tests/sbackup/test_config.py @@ -6,7 +6,8 @@ import unittest import os import tempfile import json -from sbackup.config import load_config, save_lang, Config +from unittest.mock import patch +from sbackup.config import load_config, save_lang, save_format class TestConfig(unittest.TestCase): @@ -25,6 +26,7 @@ class TestConfig(unittest.TestCase): # 清理临时目录 if os.path.exists(self.test_dir): import shutil + shutil.rmtree(self.test_dir) def test_load_config_from_file(self): @@ -33,12 +35,9 @@ class TestConfig(unittest.TestCase): """ # 创建配置文件 config_data = { - "compression": { - "algorithm": "ZIP_STORED", - "level": 1 - }, + "compression": {"algorithm": "ZIP_STORED", "level": 1}, "skip_patterns": [".git", "__pycache__", ".DS_Store"], - "data_file": "custom_sbackup.json" + "data_file": "custom_sbackup.json", } with open(self.config_file, "w", encoding="utf-8") as f: json.dump(config_data, f, ensure_ascii=False, indent=4) @@ -58,12 +57,9 @@ class TestConfig(unittest.TestCase): # 在临时目录中创建默认配置文件(而非项目根目录) default_config = os.path.join(self.test_dir, "config.json") config_data = { - "compression": { - "algorithm": "ZIP_DEFLATED", - "level": 6 - }, + "compression": {"algorithm": "ZIP_DEFLATED", "level": 6}, "skip_patterns": [".git", "__pycache__"], - "data_file": "sbackup.json" + "data_file": "sbackup.json", } with open(default_config, "w", encoding="utf-8") as f: json.dump(config_data, f, ensure_ascii=False, indent=4) @@ -105,6 +101,7 @@ class TestSaveLang(unittest.TestCase): def tearDown(self): if os.path.exists(self.test_dir): import shutil + shutil.rmtree(self.test_dir) def test_save_lang_creates_new_file(self): @@ -152,6 +149,80 @@ class TestSaveLang(unittest.TestCase): data = json.load(f) self.assertEqual(data["lang"], "zh_CN") + def test_save_lang_malformed_existing_file(self): + """测试已有配置文件 JSON 损坏时 save_lang 重置""" + with open(self.config_file, "w") as f: + f.write("{bad json!!!") + save_lang("en_US", self.config_file) + with open(self.config_file, "r", encoding="utf-8") as f: + data = json.load(f) + self.assertEqual(data["lang"], "en_US") + + @patch("os.makedirs") + def test_save_lang_makedirs_error(self, mock_makedirs): + """测试创建目录失败时 save_lang 静默返回""" + mock_makedirs.side_effect = OSError("permission denied") + save_lang("zh_CN", self.config_file) + # 不应抛出异常 + + @patch("builtins.open") + def test_save_lang_write_error(self, mock_open): + """测试写入文件失败时 save_lang 静默处理""" + mock_open.side_effect = OSError("disk full") + save_lang("zh_CN", self.config_file) + # 不应抛出异常 + + +class TestSaveFormat(unittest.TestCase): + def setUp(self): + self.test_dir = tempfile.mkdtemp() + self.config_file = os.path.join(self.test_dir, "config.json") + + def tearDown(self): + if os.path.exists(self.test_dir): + import shutil + + shutil.rmtree(self.test_dir) + + def test_save_format_creates_new_file(self): + """测试 save_format 创建新配置文件""" + save_format("tar.gz", self.config_file) + self.assertTrue(os.path.exists(self.config_file)) + with open(self.config_file, "r", encoding="utf-8") as f: + data = json.load(f) + self.assertEqual(data["compression_format"], "tar.gz") + + def test_save_format_updates_existing(self): + """测试 save_format 更新已有配置""" + with open(self.config_file, "w", encoding="utf-8") as f: + json.dump({"lang": "zh_CN"}, f) + save_format("tar.xz", self.config_file) + with open(self.config_file, "r", encoding="utf-8") as f: + data = json.load(f) + self.assertEqual(data["compression_format"], "tar.xz") + self.assertEqual(data["lang"], "zh_CN") + + def test_save_format_malformed_json(self): + """测试配置文件损坏时 save_format 重置""" + with open(self.config_file, "w") as f: + f.write("{bad") + save_format("tar.bz2", self.config_file) + with open(self.config_file, "r", encoding="utf-8") as f: + data = json.load(f) + self.assertEqual(data["compression_format"], "tar.bz2") + + @patch("os.makedirs") + def test_save_format_makedirs_error(self, mock_makedirs): + """测试创建目录失败""" + mock_makedirs.side_effect = OSError("denied") + save_format("tar.gz", self.config_file) + + @patch("builtins.open") + def test_save_format_write_error(self, mock_open): + """测试写入失败""" + mock_open.side_effect = OSError("disk full") + save_format("tar.gz", self.config_file) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/sbackup/test_i18n.py b/tests/sbackup/test_i18n.py index 6e6bc8c..118c94e 100644 --- a/tests/sbackup/test_i18n.py +++ b/tests/sbackup/test_i18n.py @@ -68,6 +68,54 @@ class TestI18n(unittest.TestCase): en_text = i18n.t("cmd.all.empty") self.assertEqual(en_text, "No backup strategies configured.") + def test_set_locale_corrupted_file_fallback(self): + """测试语言包 JSON 损坏时回退到默认中文""" + import shutil + + # 创建临时损坏的语言文件 + tmpdir = tempfile.mkdtemp() + try: + locales_dir = os.path.join(tmpdir, "locales") + os.makedirs(locales_dir) + # 创建损坏的 en_US.json + with open(os.path.join(locales_dir, "en_US.json"), "w") as f: + f.write("{corrupted!!!") + # 创建正常的 zh_CN.json 作为回退 + with open( + os.path.join(locales_dir, "zh_CN.json"), "w", encoding="utf-8" + ) as f: + json.dump({"test.key": "回退文本"}, f, ensure_ascii=False) + + # 使用 patch 模拟 + with unittest.mock.patch.object( + i18n.os.path, "dirname", return_value=tmpdir + ): + i18n.set_locale("en_US") + self.assertEqual(i18n.t("test.key"), "回退文本") + finally: + shutil.rmtree(tmpdir) + + def test_set_locale_both_corrupted(self): + """测试所有语言包都损坏时 translations 为空""" + import shutil + + tmpdir = tempfile.mkdtemp() + try: + locales_dir = os.path.join(tmpdir, "locales") + os.makedirs(locales_dir) + with open(os.path.join(locales_dir, "en_US.json"), "w") as f: + f.write("{bad") + with open(os.path.join(locales_dir, "zh_CN.json"), "w") as f: + f.write("{bad") + + with unittest.mock.patch.object( + i18n.os.path, "dirname", return_value=tmpdir + ): + i18n.set_locale("en_US") + self.assertEqual(i18n._translations, {}) + finally: + shutil.rmtree(tmpdir) + if __name__ == "__main__": unittest.main() diff --git a/tests/sbackup/test_main.py b/tests/sbackup/test_main.py index 08e67fb..de5a3e9 100644 --- a/tests/sbackup/test_main.py +++ b/tests/sbackup/test_main.py @@ -1,9 +1,11 @@ """ 单元测试 for sbackup.__init__ 模块 """ + import unittest import sys import os +import json import logging import tempfile import shutil @@ -15,14 +17,16 @@ class TestMain(unittest.TestCase): def setUp(self): self.test_dir = tempfile.mkdtemp() self.original_argv = sys.argv.copy() - # 保存并重置 logging 状态 self._root_handlers = logging.root.handlers[:] self._root_level = logging.root.level + # 创建 config.json 指向测试目录的数据文件 + self.data_path = os.path.join(self.test_dir, "sbackup.json") + with open(os.path.join(self.test_dir, "config.json"), "w") as f: + json.dump({"data_file": self.data_path}, f) def tearDown(self): sys.argv = self.original_argv shutil.rmtree(self.test_dir, ignore_errors=True) - # 恢复 logging 状态 logging.root.handlers = self._root_handlers logging.root.setLevel(self._root_level) @@ -31,6 +35,7 @@ class TestMain(unittest.TestCase): """测试 version 命令""" sys.argv = ["sbackup", "version"] from sbackup import run + run() mock_print.assert_called() @@ -39,14 +44,17 @@ class TestMain(unittest.TestCase): """测试无命令时显示帮助""" sys.argv = ["sbackup"] from sbackup import run + run() mock_help.assert_called_once() @patch("builtins.print") def test_add_command_invalid_source(self, mock_print): """测试 add 命令传入无效源目录""" + os.chdir(self.test_dir) sys.argv = ["sbackup", "--lang", "en_US", "add", "/nonexistent/path", "/tmp"] from sbackup import run + run() printed = " ".join(str(call) for call in mock_print.call_args_list) self.assertIn(t("err.folder.invalid").split("{")[0].strip(), printed) @@ -54,8 +62,10 @@ class TestMain(unittest.TestCase): @patch("builtins.print") def test_rm_command_nonexistent(self, mock_print): """测试 rm 命令删除不存在的策略""" + os.chdir(self.test_dir) sys.argv = ["sbackup", "--lang", "en_US", "rm", "/nonexistent/path"] from sbackup import run + run() printed = " ".join(str(call) for call in mock_print.call_args_list) self.assertIn(t("warn.no.strategy.found").split("{")[0].strip(), printed) @@ -66,6 +76,7 @@ class TestMain(unittest.TestCase): os.chdir(self.test_dir) sys.argv = ["sbackup", "--lang", "en_US", "all"] from sbackup import run + run() printed = " ".join(str(call) for call in mock_print.call_args_list) self.assertIn(t("cmd.all.empty"), printed) @@ -76,6 +87,7 @@ class TestMain(unittest.TestCase): os.chdir(self.test_dir) sys.argv = ["sbackup", "--lang", "en_US", "all"] from sbackup import run + run() printed = " ".join(str(call) for call in mock_print.call_args_list) self.assertIn("No backup strategies configured", printed) @@ -86,6 +98,7 @@ class TestMain(unittest.TestCase): os.chdir(self.test_dir) sys.argv = ["sbackup", "--debug", "--lang", "en_US", "all"] from sbackup import run + run() self.assertEqual(logging.root.level, logging.DEBUG) @@ -96,9 +109,156 @@ class TestMain(unittest.TestCase): logging.root.setLevel(logging.WARNING) sys.argv = ["sbackup", "--lang", "en_US", "all"] from sbackup import run + run() self.assertEqual(logging.root.level, logging.WARNING) + @patch("builtins.print") + def test_rm_command_success(self, mock_print): + """测试 rm 命令成功删除策略""" + os.chdir(self.test_dir) + sys.argv = ["sbackup", "--lang", "en_US", "add", self.test_dir, self.test_dir] + from sbackup import run + + result1 = run() + self.assertEqual(result1, 0, f"add failed with {result1}") + sys.argv = ["sbackup", "--lang", "en_US", "rm", self.test_dir] + result2 = run() + self.assertEqual(result2, 0) + + @patch("builtins.print") + def test_save_command(self, mock_print): + """测试 save 命令执行""" + os.chdir(self.test_dir) + sys.argv = ["sbackup", "--lang", "en_US", "save"] + from sbackup import run + + result = run() + self.assertEqual(result, 0) + + @patch("builtins.print") + def test_format_option(self, mock_print): + """测试 --format 参数""" + os.chdir(self.test_dir) + sys.argv = ["sbackup", "--lang", "en_US", "--format", "tar.gz", "all"] + from sbackup import run + + result = run() + self.assertEqual(result, 0) + + @patch("builtins.print") + def test_restore_command_invalid(self, mock_print): + """测试 restore 命令传入无效文件""" + os.chdir(self.test_dir) + sys.argv = ["sbackup", "--lang", "en_US", "restore", "/nonexistent.zip", "/tmp"] + from sbackup import run + + result = run() + self.assertEqual(result, 1) + + @patch("builtins.print") + @patch("time.sleep", return_value=None) + def test_watch_command(self, mock_sleep, mock_print): + """测试 watch 命令启动""" + os.chdir(self.test_dir) + sys.argv = ["sbackup", "--lang", "en_US", "watch", "--interval", "1"] + from sbackup import run + import sbackup.__init__ as init_module + + original_execute = init_module.BackupManager.execute_backups + + def mock_execute(self, *args, **kwargs): + raise KeyboardInterrupt() + + init_module.BackupManager.execute_backups = mock_execute + try: + result = run() + self.assertEqual(result, 0) + finally: + init_module.BackupManager.execute_backups = original_execute + + @patch("builtins.print") + def test_add_with_format(self, mock_print): + """测试 add 命令指定条目级格式""" + os.chdir(self.test_dir) + sys.argv = [ + "sbackup", + "--lang", + "en_US", + "add", + self.test_dir, + self.test_dir, + "--format", + "tar.gz", + ] + from sbackup import run + from sbackup.auto_save import BackupManager + + result = run() + self.assertEqual(result, 0) + # 验证数据中条目级格式已持久化 + manager = BackupManager(data_file=os.path.join(self.test_dir, "sbackup.json")) + abs_path = os.path.abspath(self.test_dir) + entry = manager._get_entry(abs_path) + assert entry is not None, "entry should not be None" + self.assertEqual(entry.compression_format, "TAR_GZ") + + def test_interval_accepts_float(self): + """测试 --interval 接受浮点数值""" + from sbackup import get_parser + + parser = get_parser() + # 解析 float 间隔值 + args = parser.parse_args(["watch", "--interval", "0.5"]) + self.assertEqual(args.interval, 0.5) + self.assertEqual(args.command, "watch") + + # 解析整数间隔值也应正常工作 + args = parser.parse_args(["watch", "--interval", "30"]) + self.assertEqual(args.interval, 30) + + def test_argparse_invalid_choice_localized(self): + """测试 argparse 无效选择错误被本地化""" + from sbackup import get_parser + from sbackup.i18n import set_locale + from io import StringIO + + # 确保 zh_CN 语言包已加载 + set_locale("zh_CN") + parser = get_parser() + old_stderr = sys.stderr + sys.stderr = StringIO() + try: + with self.assertRaises(SystemExit) as cm: + parser.parse_args(["--format", "rar", "save"]) + self.assertEqual(cm.exception.code, 2) + stderr_output = sys.stderr.getvalue() + # 检查本地化文本存在(zh_CN 默认) + self.assertIn("无效的选择", stderr_output) + finally: + sys.stderr = old_stderr + + def test_argparse_invalid_float_localized(self): + """测试 argparse 无效浮点数错误被本地化""" + from sbackup import get_parser + from sbackup.i18n import set_locale + from io import StringIO + + # 确保 zh_CN 语言包已加载 + set_locale("zh_CN") + parser = get_parser() + old_stderr = sys.stderr + sys.stderr = StringIO() + try: + with self.assertRaises(SystemExit) as cm: + parser.parse_args(["watch", "--interval", "abc"]) + self.assertEqual(cm.exception.code, 2) + stderr_output = sys.stderr.getvalue() + # 检查本地化文本存在(zh_CN 默认) + self.assertIn("无效的浮点数值", stderr_output) + finally: + sys.stderr = old_stderr + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/uv.lock b/uv.lock index 2de3b69..fd8cc09 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,280 @@ version = 1 revision = 3 -requires-python = ">=3.12" +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] + +[[package]] +name = "backports-zstd" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/36a5182ce1d8ef9ef32bff69037bd28b389bbdb66338f8069e61da7028cb/backports_zstd-1.3.0.tar.gz", hash = "sha256:e8b2d68e2812f5c9970cabc5e21da8b409b5ed04e79b4585dbffa33e9b45ebe2", size = 997138, upload-time = "2025-12-29T17:28:06.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/70/766f6ebbb9db2ed75951f0a671ee15931dc69278c84d9f09b08dd6b67c3e/backports_zstd-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a2db17a6d9bf6b4dc223b3f6414aa9db6d1afe9de9bff61d582c2934ca456a0", size = 435664, upload-time = "2025-12-29T17:25:29.201Z" }, + { url = "https://files.pythonhosted.org/packages/55/f8/7b3fad9c6ee5ff3bcd7c941586675007330197ff4a388f01c73198ecc8bb/backports_zstd-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7f16b98ba81780a9517ce6c493e1aea9b7d72de2b1efa08375136c270e1ecba", size = 362060, upload-time = "2025-12-29T17:25:30.94Z" }, + { url = "https://files.pythonhosted.org/packages/68/9e/cad0f508ed7c3fbd07398f22b5bf25aa0523fcf56c84c3def642909e80ae/backports_zstd-1.3.0-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:1124a169a647671ccb4654a0ef1d0b42d6735c45ce3d0adf609df22fb1f099db", size = 505958, upload-time = "2025-12-29T17:25:32.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/96dc55c043b0d86e53ae9608b496196936244c1ecf7e95cdf66d0dbc0f23/backports_zstd-1.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8410fda08b36202d01ab4503f6787c763898888cb1a48c19fce94711563d3ee3", size = 475571, upload-time = "2025-12-29T17:25:33.9Z" }, + { url = "https://files.pythonhosted.org/packages/20/48/d9c8c8c2a5ac57fc5697f1945254af31407b0c5f80335a175a7c215b4118/backports_zstd-1.3.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab139d1fc0e91a697e82fa834e6404098802f11b6035607174776173ded9a2cc", size = 581199, upload-time = "2025-12-29T17:25:35.566Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/7fe70d2d39ed39e26a6c6f6c1dd229f1ab889500d5c90b17527702b1a21e/backports_zstd-1.3.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f3115d203f387f77c23b5461fb6678d282d4f276f9f39298ad242b00120afc7", size = 640846, upload-time = "2025-12-29T17:25:36.86Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d8/5b8580469e70b72402212885bf19b9d31eaf23549b602e0c294edf380e25/backports_zstd-1.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:116f65cce84e215dfac0414924b051faf8d29dc7188cf3944dd1e5be8dd15a32", size = 491061, upload-time = "2025-12-29T17:25:38.721Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dd/17a752263fccd1ba24184b7e89c14cd31553d512e2e5b065f38e63a0ba86/backports_zstd-1.3.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04def169e4a9ae291298124da4e097c6d6545d0e93164f934b716da04d24630a", size = 565071, upload-time = "2025-12-29T17:25:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/df23d3fe664b2497ab2ec01dc012cb9304e7d568c67f50b1b324fb2d8cbb/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:481b586291ef02a250f03d4c31a37c9881e5e93556568abbd20ca1ad720d443f", size = 481518, upload-time = "2025-12-29T17:25:41.925Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cd/e50dd85fde890c5d79e1ed5dc241f1c45f87b6c12571fdb60add57f2ee66/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0290979eea67f7275fa42d5859cc5bea94f2c08cca6bc36396673476773d2bad", size = 509464, upload-time = "2025-12-29T17:25:43.844Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bb/e429156e4b834837fe78b4f32ed512491aea39415444420c79ccd3aa0526/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:01c699d8c803dc9f9c9d6ede21b75ec99f45c3b411821011692befca538928cb", size = 585563, upload-time = "2025-12-29T17:25:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/95/c0/1a0d245325827242aefe76f4f3477ec183b996b8db5105698564f8303481/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2c662912cfc1a5ebd1d2162ac651549d58bd3c97a8096130ec13c703fca355f2", size = 562889, upload-time = "2025-12-29T17:25:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/93/42/126b2bc7540a15452c3ebdf190ebfea8a8644e29b22f4e10e2a6aa2389e4/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3180c8eb085396928e9946167e610aa625922b82c3e2263c5f17000556370168", size = 631423, upload-time = "2025-12-29T17:25:47.81Z" }, + { url = "https://files.pythonhosted.org/packages/dc/32/018e49657411582569032b7d1bb5d62e514aad8b44952de740ec6250588d/backports_zstd-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5b9a8c75a294e7ffa18fc8425a763facc366435a8b442e4dffdc19fa9499a22c", size = 495122, upload-time = "2025-12-29T17:25:49.377Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/cdd1d2e1d3612bb90d9cf9b23bea06f2155cdafccd8b6f28a1c4d7750004/backports_zstd-1.3.0-cp310-cp310-win32.whl", hash = "sha256:845defdb172385f17123d92a00d2e952d341e9ae310bfa2410c292bf03846034", size = 288573, upload-time = "2025-12-29T17:25:51.167Z" }, + { url = "https://files.pythonhosted.org/packages/55/7c/2e9c80f08375bd14262cefa69297a926134f517c9955c0795eec5e1d470e/backports_zstd-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:43a9fea6299c801da85221e387b32d90a9ad7c62aa2a34edf525359ce5ad8f3a", size = 313506, upload-time = "2025-12-29T17:25:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5d/fa67e8174f54db44eb33498abb7f98bea4f2329e873b225391bda0113a5e/backports_zstd-1.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:df8473cb117e1316e6c6101f2724e025bd8f50af2dc009d0001c0aabfb5eb57c", size = 288688, upload-time = "2025-12-29T17:25:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/ac/28/ed31a0e35feb4538a996348362051b52912d50f00d25c2d388eccef9242c/backports_zstd-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:249f90b39d3741c48620021a968b35f268ca70e35f555abeea9ff95a451f35f9", size = 435660, upload-time = "2025-12-29T17:25:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/00/0d/3db362169d80442adda9dd563c4f0bb10091c8c1c9a158037f4ecd53988e/backports_zstd-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b0e71e83e46154a9d3ced6d4de9a2fea8207ee1e4832aeecf364dc125eda305c", size = 362056, upload-time = "2025-12-29T17:25:56.729Z" }, + { url = "https://files.pythonhosted.org/packages/bd/00/b67ba053a7d6f6dbe2f8a704b7d3a5e01b1d2e2e8edbc9b634f2702ef73c/backports_zstd-1.3.0-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:cbc6193acd21f96760c94dd71bf32b161223e8503f5277acb0a5ab54e5598957", size = 505957, upload-time = "2025-12-29T17:25:57.941Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3e/2667c0ddb53ddf28667e330bf9fe92e8e17705a481c9b698e283120565f7/backports_zstd-1.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1df583adc0ae84a8d13d7139f42eade6d90182b1dd3e0d28f7df3c564b9fd55d", size = 475569, upload-time = "2025-12-29T17:25:59.075Z" }, + { url = "https://files.pythonhosted.org/packages/eb/86/4052473217bd954ccdffda5f7264a0e99e7c4ecf70c0f729845c6a45fc5a/backports_zstd-1.3.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d833fc23aa3cc2e05aeffc7cfadd87b796654ad3a7fb214555cda3f1db2d4dc2", size = 581196, upload-time = "2025-12-29T17:26:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bd/064f6fdb61db3d2c473159ebc844243e650dc032de0f8208443a00127925/backports_zstd-1.3.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:142178fe981061f1d2a57c5348f2cd31a3b6397a35593e7a17dbda817b793a7f", size = 640888, upload-time = "2025-12-29T17:26:02.134Z" }, + { url = "https://files.pythonhosted.org/packages/d8/09/0822403f40932a165a4f1df289d41653683019e4fd7a86b63ed20e9b6177/backports_zstd-1.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5eed0a09a163f3a8125a857cb031be87ed052e4a47bc75085ed7fca786e9bb5b", size = 491100, upload-time = "2025-12-29T17:26:03.418Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a3/f5ac28d74039b7e182a780809dc66b9dbfc893186f5d5444340bba135389/backports_zstd-1.3.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60aa483fef5843749e993dde01229e5eedebca8c283023d27d6bf6800d1d4ce3", size = 565071, upload-time = "2025-12-29T17:26:05.022Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ac/50209aeb92257a642ee987afa1e61d5b6731ab6bf0bff70905856e5aede6/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ea0886c1b619773544546e243ed73f6d6c2b1ae3c00c904ccc9903a352d731e1", size = 481519, upload-time = "2025-12-29T17:26:06.255Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/b06f64199fb4b2e9437cedbf96d0155ca08aeec35fe81d41065acd44762e/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5e137657c830a5ce99be40a1d713eb1d246bae488ada28ff0666ac4387aebdd5", size = 509465, upload-time = "2025-12-29T17:26:07.602Z" }, + { url = "https://files.pythonhosted.org/packages/f4/37/2c365196e61c8fffbbc930ffd69f1ada7aa1c7210857b3e565031c787ac6/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94048c8089755e482e4b34608029cf1142523a625873c272be2b1c9253871a72", size = 585552, upload-time = "2025-12-29T17:26:08.911Z" }, + { url = "https://files.pythonhosted.org/packages/93/8d/c2c4f448bb6b6c9df17410eaedce415e8db0eb25b60d09a3d22a98294d09/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:d339c1ec40485e97e600eb9a285fb13169dbf44c5094b945788a62f38b96e533", size = 562893, upload-time = "2025-12-29T17:26:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/74/e8/2110d4d39115130f7514cbbcec673a885f4052bb68d15e41bc96a7558856/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aeee9210c54cf8bf83f4d263a6d0d6e7a0298aeb5a14a0a95e90487c5c3157c", size = 631462, upload-time = "2025-12-29T17:26:11.99Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a8/d64b59ae0714fdace14e43873f794eff93613e35e3e85eead33a4f44cd80/backports_zstd-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba7114a3099e5ea05cbb46568bd0e08bca2ca11e12c6a7b563a24b86b2b4a67f", size = 495125, upload-time = "2025-12-29T17:26:13.218Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/bcff0a091fcf27172c57ae463e49d8dec6dc31e01d7e7bf1ae3aad9c3566/backports_zstd-1.3.0-cp311-cp311-win32.whl", hash = "sha256:08dfdfb85da5915383bfae680b6ac10ab5769ab22e690f9a854320720011ae8e", size = 288664, upload-time = "2025-12-29T17:26:14.791Z" }, + { url = "https://files.pythonhosted.org/packages/28/1a/379061e2abf8c3150ad51c1baab9ac723e01cf7538860a6a74c48f8b73ee/backports_zstd-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8aac2e7cdcc8f310c16f98a0062b48d0a081dbb82862794f4f4f5bdafde30a4", size = 313633, upload-time = "2025-12-29T17:26:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/e7/eca40858883029fc716660106069b23253e2ec5fd34e86b4101c8cfe864b/backports_zstd-1.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:440ef1be06e82dc0d69dbb57177f2ce98bbd2151013ee7e551e2f2b54caa6120", size = 288814, upload-time = "2025-12-29T17:26:17.571Z" }, + { url = "https://files.pythonhosted.org/packages/72/d4/356da49d3053f4bc50e71a8535631b57bc9ca4e8c6d2442e073e0ab41c44/backports_zstd-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f4a292e357f3046d18766ce06d990ccbab97411708d3acb934e63529c2ea7786", size = 435972, upload-time = "2025-12-29T17:26:18.752Z" }, + { url = "https://files.pythonhosted.org/packages/30/8f/dbe389e60c7e47af488520f31a4aa14028d66da5bf3c60d3044b571eb906/backports_zstd-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb4c386f38323698991b38edcc9c091d46d4713f5df02a3b5c80a28b40e289ea", size = 362124, upload-time = "2025-12-29T17:26:19.995Z" }, + { url = "https://files.pythonhosted.org/packages/55/4b/173beafc99e99e7276ce008ef060b704471e75124c826bc5e2092815da37/backports_zstd-1.3.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f52523d2bdada29e653261abdc9cfcecd9e5500d305708b7e37caddb24909d4e", size = 506378, upload-time = "2025-12-29T17:26:21.855Z" }, + { url = "https://files.pythonhosted.org/packages/df/c8/3f12a411d9a99d262cdb37b521025eecc2aa7e4a93277be3f4f4889adb74/backports_zstd-1.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3321d00beaacbd647252a7f581c1e1cdbdbda2407f2addce4bfb10e8e404b7c7", size = 476201, upload-time = "2025-12-29T17:26:23.047Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/73c090e4a2d5671422512e1b6d276ca6ea0cc0c45ec4634789106adc0d66/backports_zstd-1.3.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88f94d238ef36c639c0ae17cf41054ce103da9c4d399c6a778ce82690d9f4919", size = 581659, upload-time = "2025-12-29T17:26:24.189Z" }, + { url = "https://files.pythonhosted.org/packages/08/4f/11bfcef534aa2bf3f476f52130217b45337f334d8a287edb2e06744a6515/backports_zstd-1.3.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97d8c78fe20c7442c810adccfd5e3ea6a4e6f4f1fa4c73da2bc083260ebead17", size = 640388, upload-time = "2025-12-29T17:26:25.47Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8faea426d4f49b63238bdfd9f211a9f01c862efe0d756d3abeb84265a4e2/backports_zstd-1.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eefda80c3dbfbd924f1c317e7b0543d39304ee645583cb58bae29e19f42948ed", size = 494173, upload-time = "2025-12-29T17:26:26.736Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9d/901f19ac90f3cd999bdcfb6edb4d7b4dc383dfba537f06f533fc9ac4777b/backports_zstd-1.3.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ab5d3b5a54a674f4f6367bb9e0914063f22cd102323876135e9cc7a8f14f17e", size = 568628, upload-time = "2025-12-29T17:26:28.12Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/4d29788590c2465a570c2fae49dbff05741d1f0c8e4a0fb2c1c310f31804/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7558fb0e8c8197c59a5f80c56bf8f56c3690c45fd62f14e9e2081661556e3e64", size = 482233, upload-time = "2025-12-29T17:26:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4b/24c7c9e8ef384b19d515a7b1644a500ceb3da3baeff6d579687da1a0f62b/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:27744870e38f017159b9c0241ea51562f94c7fefcfa4c5190fb3ec4a65a7fc63", size = 509806, upload-time = "2025-12-29T17:26:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7e/7ba1aeecf0b5859f1855c0e661b4559566b64000f0627698ebd9e83f2138/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b099750755bb74c280827c7d68de621da0f245189082ab48ff91bda0ec2db9df", size = 586037, upload-time = "2025-12-29T17:26:32.201Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1a/18f0402b36b9cfb0aea010b5df900cfd42c214f37493561dba3abac90c4e/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5434e86f2836d453ae3e19a2711449683b7e21e107686838d12a255ad256ca99", size = 566220, upload-time = "2025-12-29T17:26:33.5Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d9/44c098ab31b948bbfd909ec4ae08e1e44c5025a2d846f62991a62ab3ebea/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:407e451f64e2f357c9218f5be4e372bb6102d7ae88582d415262a9d0a4f9b625", size = 630847, upload-time = "2025-12-29T17:26:35.273Z" }, + { url = "https://files.pythonhosted.org/packages/30/33/e74cb2cfb162d2e9e00dad8bcdf53118ca7786cfd467925d6864732f79cc/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58a071f3c198c781b2df801070290b7174e3ff61875454e9df93ab7ea9ea832b", size = 498665, upload-time = "2025-12-29T17:26:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a9/67a24007c333ed22736d5cd79f1aa1d7209f09be772ff82a8fd724c1978e/backports_zstd-1.3.0-cp312-cp312-win32.whl", hash = "sha256:21a9a542ccc7958ddb51ae6e46d8ed25d585b54d0d52aaa1c8da431ea158046a", size = 288809, upload-time = "2025-12-29T17:26:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/42/24/34b816118ea913debb2ea23e71ffd0fb2e2ac738064c4ac32e3fb62c18bb/backports_zstd-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:89ea8281821123b071a06b30b80da8e4d8a2b40a4f57315a19850337a21297ac", size = 313815, upload-time = "2025-12-29T17:26:39.665Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2f/babd02c9fc4ca35376ada7c291193a208165c7be2455f0f98bc1e1243f31/backports_zstd-1.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:f6843ecb181480e423b02f60fe29e393cbc31a95fb532acdf0d3a2c87bd50ce3", size = 288927, upload-time = "2025-12-29T17:26:40.923Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7d/53e8da5950cdfc5e8fe23efd5165ce2f4fed5222f9a3292e0cdb03dd8c0d/backports_zstd-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e86e03e3661900955f01afed6c59cae9baa63574e3b66896d99b7de97eaffce9", size = 435463, upload-time = "2025-12-29T17:26:42.152Z" }, + { url = "https://files.pythonhosted.org/packages/da/78/f98e53870f7404071a41e3d04f2ff514302eeeb3279d931d02b220f437aa/backports_zstd-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:41974dcacc9824c1effe1c8d2f9d762bcf47d265ca4581a3c63321c7b06c61f0", size = 361740, upload-time = "2025-12-29T17:26:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ed/2c64706205a944c9c346d95c17f632d4e3468db3ce60efb6f5caa7c0dcae/backports_zstd-1.3.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:3090a97738d6ce9545d3ca5446df43370928092a962cbc0153e5445a947e98ed", size = 505651, upload-time = "2025-12-29T17:26:44.495Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7b/22998f691dc6e0c7e6fa81d611eb4b1f6a72fb27327f322366d4a7ca8fb3/backports_zstd-1.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc874638abf03ea1ff3b0525b4a26a8d0adf7cb46a448c3449f08e4abc276b3", size = 475859, upload-time = "2025-12-29T17:26:45.722Z" }, + { url = "https://files.pythonhosted.org/packages/0b/78/0cde898339a339530e5f932634872d2d64549969535447a48d3b98959e11/backports_zstd-1.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:db609e57b8ed88b3472930c87e93c08a4bbd5ffeb94608cd9c7c6f0ac0e166c6", size = 581339, upload-time = "2025-12-29T17:26:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1d/e0973e0eebe678c12c146473af2c54cda8a3e63b179785ca1a20727ad69c/backports_zstd-1.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f13033a3dd95f323c067199f2e61b4589a7880188ef4ef356c7ffbdb78a9f11", size = 642182, upload-time = "2025-12-29T17:26:48.545Z" }, + { url = "https://files.pythonhosted.org/packages/82/a2/ac67e79e137eb98aead66c7162bafe3cffcb82ef9cdeb6367ec18d88fbce/backports_zstd-1.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c4c7bcda5619a754726e7f5b391827f5efbe4bed8e62e9ec7490d42bff18aa6", size = 490807, upload-time = "2025-12-29T17:26:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/3514b1d065801ae7dce05246e9389003ed8fb1d7c3d71f85aa07a80f41e6/backports_zstd-1.3.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884a94c40f27affe986f394f219a4fd3cbbd08e1cff2e028d29d467574cd266e", size = 566103, upload-time = "2025-12-29T17:26:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/1b/03/10ddb54cbf032e5fe390c0776d3392611b1fc772d6c3cb5a9bcdff4f915f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497f5765126f11a5b3fd8fedfdae0166d1dd867e7179b8148370a3313d047197", size = 481614, upload-time = "2025-12-29T17:26:52.255Z" }, + { url = "https://files.pythonhosted.org/packages/5c/13/21efa7f94c41447f43aee1563b05fc540a235e61bce4597754f6c11c2e97/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a6ff6769948bb29bba07e1c2e8582d5a9765192a366108e42d6581a458475881", size = 509207, upload-time = "2025-12-29T17:26:53.496Z" }, + { url = "https://files.pythonhosted.org/packages/de/e7/12da9256d9e49e71030f0ff75e9f7c258e76091a4eaf5b5f414409be6a57/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1623e5bff1acd9c8ef90d24fc548110f20df2d14432bfe5de59e76fc036824ef", size = 585765, upload-time = "2025-12-29T17:26:54.99Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/59ca9cb4e7be1e59331bb792e8ef1331828efe596b1a2f8cbbc4e3f70d75/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:622c28306dcc429c8f2057fc4421d5722b1f22968d299025b35d71b50cfd4e03", size = 563852, upload-time = "2025-12-29T17:26:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ee/5a3eaed9a73bdf2c35dc0c7adc0616a99588e0de28f5ab52f3e0caaaa96f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09a2785e410ed2e812cb39b684ef5eb55083a5897bfd0e6f5de3bbd2c6345f70", size = 632549, upload-time = "2025-12-29T17:26:57.598Z" }, + { url = "https://files.pythonhosted.org/packages/75/b9/c823633afc48a1ac56d6ad34289c8f51b0234685142531bfa8197ca91777/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ade1f4127fdbe36a02f8067d75aa79c1ea1c8a306bf63c7b818bb7b530e1beaa", size = 495104, upload-time = "2025-12-29T17:26:58.826Z" }, + { url = "https://files.pythonhosted.org/packages/a3/8f/6f7030f18fa7307f87b0f57108a50a3a540b6350e2486d1739c0567629a3/backports_zstd-1.3.0-cp313-cp313-win32.whl", hash = "sha256:668e6fb1805b825cb7504c71436f7b28d4d792bb2663ee901ec9a2bb15804437", size = 288447, upload-time = "2025-12-29T17:27:00.036Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/b1df1bbbe4e6d3ffd364d0bcffdeb6c4361115c1eccd91238dbdd0c07fec/backports_zstd-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:385bdadf0ea8fe6ba780a95e4c7d7f018db7bafdd630932f0f9f0fad05d608ff", size = 313664, upload-time = "2025-12-29T17:27:01.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/0f/60918fe4d3f2881de8f4088d73be4837df9e4c6567594109d355a2d548b6/backports_zstd-1.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:4321a8a367537224b3559fe7aeb8012b98aea2a60a737e59e51d86e2e856fe0a", size = 288678, upload-time = "2025-12-29T17:27:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/35f423c0bcd85020d5e7be6ab8d7517843e3e4441071beb5c3bd8c5216cb/backports_zstd-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:10057d66fa4f0a7d3f6419ffb84b4fe61088da572e3ac4446134a1c8089e4166", size = 436155, upload-time = "2025-12-29T17:27:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/e504daea24e8916f14ecbc223c354b558d8410cfc846606668ab91d96b38/backports_zstd-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4abf29d706ba05f658ca0247eb55675bcc00e10f12bca15736e45b05f1f2d2dc", size = 362436, upload-time = "2025-12-29T17:27:05.076Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f7/06e178dbab7edb88c2872aebd68b54137e07a169eba1aeedf614014f7036/backports_zstd-1.3.0-cp313-cp313t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:127b0d73c745b0684da3d95c31c0939570810dad8967dfe8231eea8f0e047b2f", size = 507600, upload-time = "2025-12-29T17:27:06.254Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f1/2ce499b81c4389d6fa1eeea7e76f6e0bad48effdbb239da7cbcdaaf24b76/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0205ef809fb38bb5ca7f59fa03993596f918768b9378fb7fbd8a68889a6ce028", size = 475496, upload-time = "2025-12-29T17:27:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/1e/c82a586f2866aabf3a601a521af3c58756d83d98b724fda200016ac5e7e2/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c389b667b0b07915781aa28beabf2481f11a6062a1a081873c4c443b98601a7", size = 580919, upload-time = "2025-12-29T17:27:09.1Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/eb5d9b7c4cb69d1b8ccd011abe244ba6815693b70bed07ed4b77ddda4535/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8e7ac5ef693d49d6fb35cd7bbb98c4762cfea94a8bd2bf2ab112027004f70b11", size = 639913, upload-time = "2025-12-29T17:27:10.433Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/7296b99df79d9f31174a99c81c1964a32de8996ce2b3068f5bc66b413615/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d5543945aae2a76a850b23f283249424f535de6a622d6002957b7d971e6a36d", size = 494800, upload-time = "2025-12-29T17:27:11.59Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fc/b8ae6e104ba72d20cd5f9dfd9baee36675e89c81d432434927967114f30f/backports_zstd-1.3.0-cp313-cp313t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38be15ebce82737deda2c9410c1f942f1df9da74121049243a009810432db75", size = 570396, upload-time = "2025-12-29T17:27:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/56/60a7a9de7a5bc951ea1106358b413c95183c93480394f3abc541313c8679/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3e3f58c76f4730607a4e0130d629173aa114ae72a5c8d3d5ad94e1bf51f18d8", size = 481980, upload-time = "2025-12-29T17:27:14.317Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/93fc1e8e81b8ecba58b0e53a14f7b44375cf837db6354410998f0c4cb6ff/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b808bf889722d889b792f7894e19c1f904bb0e9092d8c0eb0787b939b08bad9a", size = 511358, upload-time = "2025-12-29T17:27:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0f/b165c2a6080d22306975cd86ce97270208493f31a298867e343110570370/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f7be27d56f2f715bcd252d0c65c232146d8e1e039c7e2835b8a3ad3dc88bc508", size = 585492, upload-time = "2025-12-29T17:27:16.986Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/85b4bde76e982b24a7eb57a2fb9868807887bef4d2114a3654a6530a67ef/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:cbe341c7fcc723893663a37175ba859328b907a4e6d2d40a4c26629cc55efb67", size = 568309, upload-time = "2025-12-29T17:27:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/83/64/9490667827a320766fb883f358a7c19171fdc04f19ade156a8c341c36967/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b4116a9e12dfcd834dd9132cf6a94657bf0d328cba5b295f26de26ea0ae1adc8", size = 630518, upload-time = "2025-12-29T17:27:19.525Z" }, + { url = "https://files.pythonhosted.org/packages/ea/43/258587233b728bbff457bdb0c52b3e08504c485a8642b3daeb0bdd5a76bc/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1049e804cc8754290b24dab383d4d6ed0b7f794ad8338813ddcb3907d15a89d0", size = 499429, upload-time = "2025-12-29T17:27:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/32/04/cfab76878f360f124dbb533779e1e4603c801a0f5ada72ae5c742b7c4d7d/backports_zstd-1.3.0-cp313-cp313t-win32.whl", hash = "sha256:7d3f0f2499d2049ec53d2674c605a4b3052c217cc7ee49c05258046411685adc", size = 289389, upload-time = "2025-12-29T17:27:22.287Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ff/dbcfb6c9c922ab6d98f3d321e7d0c7b34ecfa26f3ca71d930fe1ef639737/backports_zstd-1.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eb2f8fab0b1ea05148394cb34a9e543a43477178765f2d6e7c84ed332e34935e", size = 314776, upload-time = "2025-12-29T17:27:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/01/4b/82e4baae3117806639fe1c693b1f2f7e6133a7cefd1fa2e38018c8edcd68/backports_zstd-1.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c66ad9eb5bfbe28c2387b7fc58ddcdecfb336d6e4e60bcba1694a906c1f21a6c", size = 289315, upload-time = "2025-12-29T17:27:24.601Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/e843d32122f25d9568e75d1e7a29c00eae5e5728015604f3f6d02259b3a5/backports_zstd-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3ab0d5632b84eff4355c42a04668cfe6466f7d390890f718978582bd1ff36949", size = 409771, upload-time = "2025-12-29T17:27:48.869Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a5/d6a897d4b91732f54b4506858f1da65d7a5b2dc0dbe36a23992a64f09f5a/backports_zstd-1.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b97cea95dbb1a97c02afd718155fad93f747815069722107a429804c355e206", size = 339289, upload-time = "2025-12-29T17:27:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b0/f0ce566ec221b284508eebbf574a779ba4a8932830db6ea03b6176f336a2/backports_zstd-1.3.0-pp310-pypy310_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:477895f2642f9397aeba69618df2c91d7f336e02df83d1e623ac37c5d3a5115e", size = 420335, upload-time = "2025-12-29T17:27:51.455Z" }, + { url = "https://files.pythonhosted.org/packages/62/6d/bf55652c84c79b2565d3087265bcb097719540a313dee16359a54d83ab4e/backports_zstd-1.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:330172aaf5fd3bfa53f49318abc6d1d4238cb043c384cf71f7b8f0fe2fb7ce31", size = 393880, upload-time = "2025-12-29T17:27:52.869Z" }, + { url = "https://files.pythonhosted.org/packages/be/e0/d1feebb70ffeb150e2891c6f09700079f4a60085ebc67529eb1ca72fb5c2/backports_zstd-1.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32974e71eff15897ed3f8b7766a753d9f3197ea4f1c9025d80f8de099a691b99", size = 413840, upload-time = "2025-12-29T17:27:54.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/28/3b7be27ae51e418d3a724bbc4cb7fea77b6bd38b5007e333a56b0cb165c8/backports_zstd-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:993e3a34eaba5928a2065545e34bf75c65b9c34ecb67e43d5ef49b16cc182077", size = 299685, upload-time = "2025-12-29T17:27:56.149Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d9/8c9c246e5ea79a4f45d551088b11b61f2dc7efcdc5dbe6df3be84a506e0c/backports_zstd-1.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:968167d29f012cee7b112ad031a8925e484e97e99288e55e4d62962c3a1013e3", size = 409666, upload-time = "2025-12-29T17:27:57.37Z" }, + { url = "https://files.pythonhosted.org/packages/a4/4f/a55b33c314ca8c9074e99daab54d04c5d212070ae7dbc435329baf1b139e/backports_zstd-1.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8f6fc7d62b71083b574193dd8fb3a60e6bb34880cc0132aad242943af301f7a", size = 339199, upload-time = "2025-12-29T17:27:58.542Z" }, + { url = "https://files.pythonhosted.org/packages/9d/13/ce31bd048b1c88d0f65d7af60b6cf89cfbed826c7c978f0ebca9a8a71cfc/backports_zstd-1.3.0-pp311-pypy311_pp73-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:e0f2eca6aac280fdb77991ad3362487ee91a7fb064ad40043fb5a0bf5a376943", size = 420332, upload-time = "2025-12-29T17:28:00.332Z" }, + { url = "https://files.pythonhosted.org/packages/cf/80/c0cdbc533d0037b57248588403a3afb050b2a83b8c38aa608e31b3a4d600/backports_zstd-1.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:676eb5e177d4ef528cf3baaeea4fffe05f664e4dd985d3ac06960ef4619c81a9", size = 393879, upload-time = "2025-12-29T17:28:01.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/38/c97428867cac058ed196ccaeddfdf82ecd43b8a65965f2950a6e7547e77a/backports_zstd-1.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:199eb9bd8aca6a9d489c41a682fad22c587dffe57b613d0fe6d492d0d38ce7c5", size = 413842, upload-time = "2025-12-29T17:28:03.113Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ec/6247be6536668fe1c7dfae3eaa9c94b00b956b716957c0fc986ba78c3cc4/backports_zstd-1.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2524bd6777a828d5e7ccd7bd1a57f9e7007ae654fc2bd1bc1a207f6428674e4a", size = 299684, upload-time = "2025-12-29T17:28:04.856Z" }, +] + +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/10/a090475284fc4a71aed40a96f32e44a7fe5bda39687353dd977720b211b6/brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", size = 863089, upload-time = "2025-11-05T18:38:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/03/41/17416630e46c07ac21e378c3464815dd2e120b441e641bc516ac32cc51d2/brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", size = 445442, upload-time = "2025-11-05T18:38:02.434Z" }, + { url = "https://files.pythonhosted.org/packages/24/31/90cc06584deb5d4fcafc0985e37741fc6b9717926a78674bbb3ce018957e/brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", size = 1532658, upload-time = "2025-11-05T18:38:03.588Z" }, + { url = "https://files.pythonhosted.org/packages/62/17/33bf0c83bcbc96756dfd712201d87342732fad70bb3472c27e833a44a4f9/brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", size = 1631241, upload-time = "2025-11-05T18:38:04.582Z" }, + { url = "https://files.pythonhosted.org/packages/48/10/f47854a1917b62efe29bc98ac18e5d4f71df03f629184575b862ef2e743b/brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", size = 1424307, upload-time = "2025-11-05T18:38:05.587Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b7/f88eb461719259c17483484ea8456925ee057897f8e64487d76e24e5e38d/brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", size = 1488208, upload-time = "2025-11-05T18:38:06.613Z" }, + { url = "https://files.pythonhosted.org/packages/26/59/41bbcb983a0c48b0b8004203e74706c6b6e99a04f3c7ca6f4f41f364db50/brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d", size = 1597574, upload-time = "2025-11-05T18:38:07.838Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e6/8c89c3bdabbe802febb4c5c6ca224a395e97913b5df0dff11b54f23c1788/brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", size = 1492109, upload-time = "2025-11-05T18:38:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/4b19d4310b2dbd545c0c33f176b0528fa68c3cd0754e34b2f2bcf56548ae/brotli-1.2.0-cp310-cp310-win32.whl", hash = "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997", size = 334461, upload-time = "2025-11-05T18:38:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/ac/39/70981d9f47705e3c2b95c0847dfa3e7a37aa3b7c6030aedc4873081ed005/brotli-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196", size = 369035, upload-time = "2025-11-05T18:38:11.827Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" }, + { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" }, + { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" }, + { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, +] + +[[package]] +name = "brotlicffi" +version = "1.2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/f9/dfa56316837fa798eac19358351e974de8e1e2ca9475af4cb90293cd6576/brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd", size = 433046, upload-time = "2026-03-05T19:53:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f5/f8f492158c76b0d940388801f04f747028971ad5774287bded5f1e53f08d/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5", size = 1541126, upload-time = "2026-03-05T19:53:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e1/ff87af10ac419600c63e9287a0649c673673ae6b4f2bcf48e96cb2f89f60/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac", size = 1541983, upload-time = "2026-03-05T19:53:50.317Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/80ecd9bd45776109fab14040e478bf63e456967c9ddee2353d8330ed8de1/brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec", size = 349047, upload-time = "2026-03-05T19:53:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/98/13e5b250236a281b6cd9e92a01ee1ae231029fa78faee932ef3766e1cb24/brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000", size = 385652, upload-time = "2026-03-05T19:53:53.892Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/7f/53/6262c2256513e6f530d81642477cb19367270922063eaa2d7b781d8c723d/brotlicffi-1.2.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851", size = 402265, upload-time = "2026-03-05T19:54:05.858Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d9/d5340b43cf5fbe7fe5a083d237e5338cc1caa73bea523be1c5e452c26290/brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37cb587d32bf7168e2218c455e22e409ad1f3157c6c71945879a311f3e6b6abf", size = 406710, upload-time = "2026-03-05T19:54:07.272Z" }, + { url = "https://files.pythonhosted.org/packages/a3/82/dbced4c1e0792efdf23fd90ff6d2a320c64ff4dfef7aacc85c04fde9ddd2/brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d6ba65dd528892b4d9960beba2ae011a753620bcfc66cf6fa3cee18d7b0baa4", size = 402787, upload-time = "2026-03-05T19:54:08.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6f/534205ba7590c9a8716a614f270c5c2ec419b5b7079b3f9cd31b7b5580de/brotlicffi-1.2.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1", size = 375108, upload-time = "2026-03-05T19:54:10.079Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] [[package]] name = "colorama" @@ -11,16 +285,452 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[[package]] +name = "inflate64" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/f3/41bb2901543abe7aad0b0b0284ae5854bb75f848cf406bf8a046bf525f67/inflate64-1.0.4.tar.gz", hash = "sha256:b398c686960c029777afc0ed281a86f66adb956cfc3fbf6667cc6453f7b407ce", size = 902542, upload-time = "2025-11-28T10:55:52.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/d5/5c13cfc7954ed716ae0e5e64c4f54be43f8c145b546472b67803feaa18a4/inflate64-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1a47837d4322e0684824f91eb635aa6fd1967584140c478b0a1aca7b11740d6", size = 58602, upload-time = "2025-11-28T10:54:21.346Z" }, + { url = "https://files.pythonhosted.org/packages/33/57/4d740b677cda81ec6f47c05b502ed15103c8a7d9c3e91ee93352d46fe69c/inflate64-1.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8600478542e2354d1ee7b5c57c957006cabacd8b787b4046951f487a2216e5c0", size = 35856, upload-time = "2025-11-28T10:54:22.629Z" }, + { url = "https://files.pythonhosted.org/packages/17/cd/ec3c058283706a43ab790e8d611a3a787a4f4cc4ae3faeafba6e2e216e36/inflate64-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb2b5a62579d074f38352a3494c3c6ac1a90516b75c5793c39303547f1fea925", size = 36007, upload-time = "2025-11-28T10:54:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/24/83/90f7086f8078057a090db43459e478dc45e2d5ce2509f9c6a6a08100efa0/inflate64-1.0.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dcfafc572a642215894af1ec8d05949fa35eb7cb36d053aa97b11eccf1ae579e", size = 95055, upload-time = "2025-11-28T10:54:24.864Z" }, + { url = "https://files.pythonhosted.org/packages/95/d3/4f760f095ce8a9494d441b96a8735346141dd24f52fa573c971c0da1c958/inflate64-1.0.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb93159cb60aee8cab62541aa70e4c460f13359660a27a1a486518bba0153535", size = 96645, upload-time = "2025-11-28T10:54:26.308Z" }, + { url = "https://files.pythonhosted.org/packages/8c/2a/78ab2fcb02c13e3c8c93c2d82bf5eec1862b428bc6177dcc76ac4044408d/inflate64-1.0.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:89126ceb4d96e76842f4697017a9a3e750c34e029ddb360b3d8ca79a648d47f6", size = 92933, upload-time = "2025-11-28T10:54:27.563Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1b/c9a2d84fc117dddee0749dc1b3ab9ed725bf92e866ef0ede0945a5128ef0/inflate64-1.0.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f70e6692617ec82500b203eefac8765302298ce7e73584fcf995bb9e23184530", size = 95898, upload-time = "2025-11-28T10:54:28.91Z" }, + { url = "https://files.pythonhosted.org/packages/bd/85/5879fa47122c7d5b563c6dacbd4a782bd9464405f69d46af01e02d4a3907/inflate64-1.0.4-cp310-cp310-win32.whl", hash = "sha256:d08cdda33341b4f992af60c12dc60e370e9993b80a936c17244a602711eeb727", size = 32940, upload-time = "2025-11-28T10:54:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/a3/59/cef1b3505dc33d8cb9d115481923dec1de1372d29ac278622feecf9c03a1/inflate64-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:950dd7fe53474df5f4699b8f099980027e812d55fd82d8e167d599822c3d27d6", size = 35541, upload-time = "2025-11-28T10:54:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/72/13/d964fbfceeee6752c36c45645e5e9a9ef0dd70d4ce64e5e7316822e43382/inflate64-1.0.4-cp310-cp310-win_arm64.whl", hash = "sha256:bad20de249d6336793f6267880668dbb286ca5c6e6991795aa6344c817588068", size = 33460, upload-time = "2025-11-28T10:54:32.954Z" }, + { url = "https://files.pythonhosted.org/packages/39/e4/2fc07d71cf863ed4167e7d3eb7f89de0341ffe3ed3a62ff6cc4123bdbda6/inflate64-1.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bccda9815b27623e805a34ee3ee4f46c93f0cc7ac621f9834d75f033fd79c27a", size = 58595, upload-time = "2025-11-28T10:54:34.338Z" }, + { url = "https://files.pythonhosted.org/packages/53/77/1119bb53e8f4c9c77f2a5e3ab7d8c3e905fcfc9912073962b9b4cbf72118/inflate64-1.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c11e2a3cb9d9b49620c9b0c806dd0c55daec3b6bb665299b770a68f01bfc5432", size = 35854, upload-time = "2025-11-28T10:54:35.796Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/8b028a731f6fabbb49069a58f1aa5c3332688b57dcb8726f9e596661ce5b/inflate64-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e42def03ace8c58fd50b0df4f40241c45a2314c3876d020cce24acf958323c98", size = 36011, upload-time = "2025-11-28T10:54:37.208Z" }, + { url = "https://files.pythonhosted.org/packages/86/a3/5b67ef7fd5e7546d4c2be8a9a869c70d9fd525de1cfb2b4dc4b0855eeee8/inflate64-1.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7912927a509ca58d1a445ce4ff6e6e9f276dc1d72687386cdf7103bf590e785c", size = 98112, upload-time = "2025-11-28T10:54:38.616Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6e/6443ea6c42f6b244c4e59cb43f16e6b903669f718a96e3f1985acf473dea/inflate64-1.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec40c0383cbd84d845dcb785a48ae76eef43246c923f84fda380fdd5ea653d3c", size = 99549, upload-time = "2025-11-28T10:54:40.167Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ce/e98577e0771b857b491a64a3f7495eb507e0752b80093068b69813088df1/inflate64-1.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b01539fea372c6078b9707d9121c12cb321e587e193f50e257ce06cf5b15e41", size = 95946, upload-time = "2025-11-28T10:54:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3e/8165ff4051dff08b2cc8448dafd15a697564e84cf40f3ee0dc0df16eed16/inflate64-1.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bf4e34e32a37a42e9cf8bd9681f89e3e37b218f97d8b8cc95bd065419bc8db13", size = 98766, upload-time = "2025-11-28T10:54:43.366Z" }, + { url = "https://files.pythonhosted.org/packages/79/b8/20cad309ded1d97195c5ad50e341b38ddc2e82f5c44ee7d000c4372c8b56/inflate64-1.0.4-cp311-cp311-win32.whl", hash = "sha256:2725ccc14b138f0ad622d0322b769f177f9edfe016ee9ed3404102935d39e7de", size = 32939, upload-time = "2025-11-28T10:54:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/8c/32/be5cac018960157d33d61278377d590d5cc34922222cb0c4dc3284ce6eeb/inflate64-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:7056148548c1f25dcb38251f88c19b4635a5f32af4c7bad00621c85509e3d8c0", size = 35535, upload-time = "2025-11-28T10:54:46.214Z" }, + { url = "https://files.pythonhosted.org/packages/13/36/9b130e45299d587f306178d65e950e1c8f60a09db8bb55c7cdce8fdda3b6/inflate64-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:2ea7bdcad65e255b4596f84880f6e0c1756d6336d620e302653257defa407742", size = 33457, upload-time = "2025-11-28T10:54:47.258Z" }, + { url = "https://files.pythonhosted.org/packages/ed/33/5cfa7468960de1be0833e7e41adf5b7804a0aef2fb46f3679df3876bf3ab/inflate64-1.0.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8009e4a4918ee6c8cbc49e58fe159464895064cfdf0565fed3f49ca81e45272", size = 58619, upload-time = "2025-11-28T10:54:48.315Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0a/583c7c2832da36e986c5758d0afb6f5944599e55c5b798b066a9ef63e581/inflate64-1.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d173a7a0e865bb7d19685c5b1ad2994712b8361b24136d7e94abeff58505647", size = 35865, upload-time = "2025-11-28T10:54:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/9c6acfbe900e5c8698132244c68036b0455bd2169f46e356c83dc0366f11/inflate64-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8bad992f2d034f5f7e36208e54502d1b0829ce772c898e5dc59109833420148a", size = 36019, upload-time = "2025-11-28T10:54:50.813Z" }, + { url = "https://files.pythonhosted.org/packages/7f/66/c0c3d3b4b863aab2c2ce631d219a8eb3b95b78acd5f40d3212f071e693db/inflate64-1.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bfcf806912ced77a21394f7363805ecacd626b79f93cba87d505a48e88ede78", size = 98765, upload-time = "2025-11-28T10:54:52.273Z" }, + { url = "https://files.pythonhosted.org/packages/37/00/1a2351a85d36b26c5b2b8cfbb37ad86084c98f592dd7590f8577d8b33993/inflate64-1.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62d1aac3aba094ae42e27ce7581b414c90f218248be0953b6aeb11a127225e5d", size = 100594, upload-time = "2025-11-28T10:54:53.684Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3e/5d18ff5b86aaaf54117e1bb6ce15cb17163f56035f9c480e609d35f258ab/inflate64-1.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8065166f355122484f004225b379d403346bdae69ec624786a9334f025580675", size = 96745, upload-time = "2025-11-28T10:54:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/306d5d6aca1e04e596d2a504a59ff9a900623a6ec852f38aab99f384562d/inflate64-1.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94a95f32087d223d2e119ff5c7c264109e8d4cb7e421e7a688a899a6fe021b38", size = 99795, upload-time = "2025-11-28T10:54:56.485Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ec/d4caa4bf3c9e520c15e900fee5a00fc523953843e14aa378ca1abfb2b4ea/inflate64-1.0.4-cp312-cp312-win32.whl", hash = "sha256:ad4fa490bb7dc2a4640a3adaa2d5950f4a465ba034bbcf184c2103646e58ad97", size = 32956, upload-time = "2025-11-28T10:54:57.642Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/c0de4e9bdf12e449360b710e9ab5b5248804610f382b538773cbd07b72bb/inflate64-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:2c6befdf83d088a6e0d10d0873a9d4bfde2ce00ad7a52c8189cf303306f98030", size = 35577, upload-time = "2025-11-28T10:54:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/03a5ef74ad3869b3c5af3b09216321f5a1a5a45265f7bd6d5abc669c7622/inflate64-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:2b263c619469f90a75f29c421c53d31b208ad494a078235a8f6db2bc96583fdc", size = 33465, upload-time = "2025-11-28T10:55:00.568Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/b7de7ae318a4f233f892c4f7c8b7e0e8643abe3fdcc53ed35020a9fe3f47/inflate64-1.0.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3f37540d0e64884a935fd62a7d17e40ab69f05ec63e815483b6513675d01bef", size = 58633, upload-time = "2025-11-28T10:55:01.931Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5f/6f89c8524503fd7a9ca2bd91fe60d7291b3f684e9d41edb38ef49e10fc3d/inflate64-1.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4d24112180c95d12f279cade9a1e21f8be7f4790c4109c293292edf87d061992", size = 35864, upload-time = "2025-11-28T10:55:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/7d/14/9eadd59244b38cf85ccd0ca43d1296c50b3a33aa37be4fb68a1928efa58c/inflate64-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5c098dab17821f466fc6e6a3d78fc6e0295bb51458015f03416b1d58d6a8df4f", size = 36019, upload-time = "2025-11-28T10:55:04.126Z" }, + { url = "https://files.pythonhosted.org/packages/ca/04/399e82d8f5003dd92c8a0c5c1a9a8ce0919114710a496cbe88848bff3a72/inflate64-1.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a984b9287ff0fb596eb058d66a9e94530556afd2b7c054b44f2e0aeeff894e8f", size = 98973, upload-time = "2025-11-28T10:55:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/038dc2593bbc4272d87eac8c9f75692267d47f834ced888f6d81995df606/inflate64-1.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f62a13d0327631778fa2a47c308ae2b07b2659b7bb8564783259ac65949f8c0c", size = 100777, upload-time = "2025-11-28T10:55:06.41Z" }, + { url = "https://files.pythonhosted.org/packages/74/4a/f6d3031dd3578510894a41bfe1ac149228970ce1629a43f20e0c5abbe8d1/inflate64-1.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:513201336fb3b0b7e2aee5dbbbe30a9f1b23291738b5ceb80076fc285f2ec2f1", size = 96967, upload-time = "2025-11-28T10:55:07.594Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/7ab3bde4e176609ae0e607a8a9bb38d201885275664a6d574299f5bf7850/inflate64-1.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84ce3a97272ba745fce52b38363855c7201968f6402a794bbade774e64c657b9", size = 99997, upload-time = "2025-11-28T10:55:08.85Z" }, + { url = "https://files.pythonhosted.org/packages/39/59/8256b3e802e203c8645e0b32b25e6bd94508ad572593f0cdf8234db3879a/inflate64-1.0.4-cp313-cp313-win32.whl", hash = "sha256:332051a9d7e50579b90a3f555d68f53414b06f636c9ffe82e97c0baae3c8fbcc", size = 32958, upload-time = "2025-11-28T10:55:10.343Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4f/5784ee1eb8260f2310e24ef2883f1f494f9332bcfde4ed14ee780372149e/inflate64-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:3983f53b590ff7d0ba243f664ce852aca882482f30f7a8eab33e10d769336d0c", size = 35578, upload-time = "2025-11-28T10:55:11.389Z" }, + { url = "https://files.pythonhosted.org/packages/39/e8/8d927770ce25dc9764c8104207a80653d65471d0a6a8f9ead350016e4586/inflate64-1.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:118d8286f085e99a14341c76ef9fbffd56619ccc80318a9a204aea3dbfa71470", size = 33465, upload-time = "2025-11-28T10:55:12.824Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fb/ec9d10f44f2fc7666ad5d70acae2b8a1941e8e08ccae1fad0820f7796be3/inflate64-1.0.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4f61925b2d4248eac2ebb15350a80aaa0d1f7f1dc770bd5ebbbb3b0db4a6a416", size = 58727, upload-time = "2025-11-28T10:55:13.834Z" }, + { url = "https://files.pythonhosted.org/packages/81/80/24ba0d2ee14e07e275e9c5b058e59a8a58f8ef42dd51a78ebbfd7c857ac4/inflate64-1.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c1acf18b08b32981a4a11ec5a112b8ad5d7c7a5b15cb5bdbdb5b19201e9aa180", size = 35945, upload-time = "2025-11-28T10:55:15.225Z" }, + { url = "https://files.pythonhosted.org/packages/70/b8/073a79716e093db973b8823bdfb02e10fbdf65642dbe1fa3cda24832aeb2/inflate64-1.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:abddae8920b2eaef824254e14b8d4ff54afbe6194a1bbe9816584859f0c1244d", size = 36060, upload-time = "2025-11-28T10:55:16.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f0/87d3c317ed0acd94f5e9b3b1b9e9000228ea2af0cb4618c62cbfc816da34/inflate64-1.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b303132cc562a906543a56f35c4e164e3880da6ff041cb4a7b1df9f9d2b4bb69", size = 98995, upload-time = "2025-11-28T10:55:17.405Z" }, + { url = "https://files.pythonhosted.org/packages/90/72/0b6035302e9c33f004240a50cb6e2e1fc7bb1f2b415b02d939c551bdd06b/inflate64-1.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f0993214dea0738c557fa56c13cd9083aef0097a201d726c21984ad7f577514", size = 100781, upload-time = "2025-11-28T10:55:18.597Z" }, + { url = "https://files.pythonhosted.org/packages/2b/05/5f383c615ec0f01bcbbc699a71da167623e494083ab7ed0df86b4bddf125/inflate64-1.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a6baedc3288d7a4ff588951d3a9a97a5391dceed6255ff5b16e42cae7274bfa9", size = 97038, upload-time = "2025-11-28T10:55:20.156Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9a/c1482718c717c49c67490c42b4fdced9476e894eaebd52193e488c12e188/inflate64-1.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a846ce1f38845b20bef2625af1b512be83416d97824539524c5a34e7a729aec7", size = 100016, upload-time = "2025-11-28T10:55:21.337Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7f/700ede7474e72a1c0e2e8fe36624cd0225ca8b2875eca33d64aa2de75f4d/inflate64-1.0.4-cp314-cp314-win32.whl", hash = "sha256:eef87908c780439393d577a155868317f0a275b47b417db9f47d8633ec791745", size = 33691, upload-time = "2025-11-28T10:55:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/f2972df8cceecc9bf3afa3353d517ffc7125285198c844588e9aaf98f5d0/inflate64-1.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:fb2fdd63ef3933b67af98b3f2ee2f57e7787278041d7ba4821382fedd729b68a", size = 36304, upload-time = "2025-11-28T10:55:24.005Z" }, + { url = "https://files.pythonhosted.org/packages/29/77/16200aced67215119fb30ec9a5889b48289ee2fa5ce5137623a9ad41b2c4/inflate64-1.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:2e129669a0243ac7816fd526946ee01c25688fe81623a6d6bc95b3156d80f4fb", size = 34491, upload-time = "2025-11-28T10:55:25.068Z" }, + { url = "https://files.pythonhosted.org/packages/c0/79/b466ec7666c40912ea81a305a8a2b75f5998e6cec1d3d75e067d76203731/inflate64-1.0.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b17bf665d948dc4edeea0cd17752415d0cd7240c882b9c7e136ad4cc4321e9d4", size = 59300, upload-time = "2025-11-28T10:55:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/16e1168ac80f39894e6cb18b439eec63fec42cbced239aebfe12081b6ec7/inflate64-1.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6751758301936fbb38fa38eb5312e14e27b6a1abf568f83c17557fab2694373d", size = 36258, upload-time = "2025-11-28T10:55:27.295Z" }, + { url = "https://files.pythonhosted.org/packages/25/42/c463b42fd8a7947b4445fbaf57c265bb7f3114362fb7aee6884ffd8b5341/inflate64-1.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d6a4136752aa2a544301059d8f13780aeb88c34d60770258436a87dacd3fc304", size = 36296, upload-time = "2025-11-28T10:55:28.632Z" }, + { url = "https://files.pythonhosted.org/packages/39/f1/cf6121926e405020e9e7bccb78ec7781fbc87500ec67368e7d9e866758be/inflate64-1.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:938ebc6b28578bfd365d1a9fdb18b7faab08321babeb2198e8025d07d8dc7fb5", size = 106788, upload-time = "2025-11-28T10:55:29.797Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dd/b653f9962497cf4d3520d69272894c37fd76f86a0e04bb3bd9f32827dc2f/inflate64-1.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61f51f80fa6f367288343c1a2cd20a42af454883087064e9274fd2a8c3a5a200", size = 107959, upload-time = "2025-11-28T10:55:31.306Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/65d88109b63611c9b6c29008201107127cf2603d186ab99beec39d85f38e/inflate64-1.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:172b51da7bbfa66b33f0a5405e944807b9949e92cf4cd9f983c07af8152766df", size = 104213, upload-time = "2025-11-28T10:55:32.506Z" }, + { url = "https://files.pythonhosted.org/packages/f9/94/c17de2f55b9fb1269bca4657f9089efe4ba0f3d4b652f07b34dfc69f69a2/inflate64-1.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ca9a2985afd5a14fb48cd126a67e5944ccb7a0a6bdec58c4f796c8c88a84539", size = 106629, upload-time = "2025-11-28T10:55:33.735Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/8bbc587bb2ad09ab7edf1f9215b2c5faf4fa7ee7c071daefe9ed55e28814/inflate64-1.0.4-cp314-cp314t-win32.whl", hash = "sha256:f8964ceaabea294bc20abc9ef408c6aae978a75c25c83168a76cd87a37c38938", size = 33865, upload-time = "2025-11-28T10:55:35.335Z" }, + { url = "https://files.pythonhosted.org/packages/91/87/8bf6f412f93c8b6dc14866a021b83321331fbdd17f6ab902a24dbf88773d/inflate64-1.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:7d13b04cba65c12d21e65eaa77da9484e265e8e821b26e0761d1455ad3a878d9", size = 36943, upload-time = "2025-11-28T10:55:36.983Z" }, + { url = "https://files.pythonhosted.org/packages/7d/85/33447bb3c4e3c0ae7b1fde3aadc52a18b2b0193cfcf4f585977e924c6463/inflate64-1.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:9ae3ee727235a06dc3cd353ee5761fdd8e3b56ad119c711f61680528972a6ced", size = 34846, upload-time = "2025-11-28T10:55:38.433Z" }, +] + +[[package]] +name = "multivolumefile" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/f0/a7786212b5a4cb9ba05ae84a2bbd11d1d0279523aea0424b6d981d652a14/multivolumefile-0.2.3.tar.gz", hash = "sha256:a0648d0aafbc96e59198d5c17e9acad7eb531abea51035d08ce8060dcad709d6", size = 77984, upload-time = "2021-04-29T12:18:39.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/31/ec5f46fd4c83185b806aa9c736e228cb780f13990a9cf4da0beb70025fcc/multivolumefile-0.2.3-py3-none-any.whl", hash = "sha256:237f4353b60af1703087cf7725755a1f6fcaeeea48421e1896940cd1c920d678", size = 17037, upload-time = "2021-04-29T12:18:38.886Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "py7zr" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-zstd", marker = "python_full_version < '3.14'" }, + { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, + { name = "brotlicffi", marker = "platform_python_implementation == 'PyPy'" }, + { name = "inflate64" }, + { name = "multivolumefile" }, + { name = "psutil", marker = "sys_platform != 'cygwin'" }, + { name = "pybcj" }, + { name = "pycryptodomex" }, + { name = "pyppmd" }, + { name = "texttable" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/e6/01fb15361ca75ee5d01df6361825a49816a836c99980c5481da0e40c6877/py7zr-1.1.0.tar.gz", hash = "sha256:087b1a94861ad9eb4d21604f6aaa0a8986a7e00580abd79fedd6f82fecf0592c", size = 70855, upload-time = "2025-12-21T03:27:44.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/9c/762284710ead9076eeecd55fb60509c19cd1f4bea811df5f3603725b44cb/py7zr-1.1.0-py3-none-any.whl", hash = "sha256:5921bc30fb72b5453aafe3b2183664c08ef508cde2655988d5e9bd6078353ef7", size = 71257, upload-time = "2025-12-21T03:27:42.881Z" }, +] + +[[package]] +name = "pybcj" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/2670b672655b18454841b8e88f024b9159d637a4c07f6ce6db85accf8467/pybcj-1.0.7.tar.gz", hash = "sha256:72d64574069ffb0a800020668376b7ebd7adea159adbf4d35f8effc62f0daa67", size = 31282, upload-time = "2025-11-29T00:53:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/8b/8dad8e96268349363184a77ca7a0f9ab3941d0e84e41d75fd4be8ac25494/pybcj-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:618ec7345775c306d83527750e2d0ab3f42ffdc5ad6282f62f88cb53c9b2b679", size = 31780, upload-time = "2025-11-29T00:52:22.317Z" }, + { url = "https://files.pythonhosted.org/packages/98/14/dc0cc7b4f876c733519956a764a64b4fa46b0da353a578f5eacdc5a24897/pybcj-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7e7faa1b0f7d894685e4567dd41268b93df89cff347ebfdfdc48b4bc0d68cb2", size = 23690, upload-time = "2025-11-29T00:52:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bf/102510783410dde5aadbc4caa98a1d45dba2c9c304ff02320c0297456a6a/pybcj-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3cd4b2d05272df605d5bdb54b3386985a2b074b4d97072da944736abd639fdee", size = 24072, upload-time = "2025-11-29T00:52:25.419Z" }, + { url = "https://files.pythonhosted.org/packages/24/da/1fe5aa16188260408e21e850268a758863cf9683049f29b03336e46585dc/pybcj-1.0.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8eb5cd6f52df8857a8d9de594ca28a71683169b9de5af7e727c0e510aedb4550", size = 50614, upload-time = "2025-11-29T00:52:26.748Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6e/5bfc159a996a8c009cc3ac2dbb1d2603f3afefbf03f674ec7f784448fb57/pybcj-1.0.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9d10760356b7d254b7b04ff38e052d5229c5f5a69a5514c9c31cb1dbb7d7f82", size = 49451, upload-time = "2025-11-29T00:52:28.189Z" }, + { url = "https://files.pythonhosted.org/packages/89/85/050a9dd0aaacce88fb223a9842fb8f58674e0f485c3ccf65aa08bfef07ff/pybcj-1.0.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1696d9b50971e317f72802bebd18f9b53684892ad3c43e0258f34e0a01738484", size = 49738, upload-time = "2025-11-29T00:52:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/73/c9/c6b06dd5845ca1c00e7b2ea2ba5bddfd660a0c7fcf7168b67af1b9d2ba76/pybcj-1.0.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e3d91b5dfdb0a200545b68145d81dae4edd2c385d89643dc45d6d01291f5c04", size = 49112, upload-time = "2025-11-29T00:52:30.787Z" }, + { url = "https://files.pythonhosted.org/packages/32/80/bd8c01fe6804319a19bcdacf0c725b898508696705b0e17cdaf1abee59f3/pybcj-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:74df8f4c897f937105e8cd830df3b4ddf65ab5b5ba3e63cd6e3aeb3f4ecb0864", size = 24900, upload-time = "2025-11-29T00:52:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/95/b9/5d70116ea216474077fe6e9c8b326a576d367bcb3c1ec03255ccab33a600/pybcj-1.0.7-cp310-cp310-win_arm64.whl", hash = "sha256:dc121ecb26fdc1a4173a20b3c7cca5d8cc81494b485d4b44a62ed8448f8c796e", size = 23396, upload-time = "2025-11-29T00:52:33.216Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c7/93567f1a9624b41e7755978243f7abbf198e153242ff7737862376edf468/pybcj-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:906ee707e89302253813a123f90a36d94d1f3c8785a4a1b853b31ac67296857a", size = 31780, upload-time = "2025-11-29T00:52:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f6/c96a25b4ded3eabe58b07e05528cca0c0ff5b5142a67dc117979d43ae2c2/pybcj-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93da8503161fd51e01843aca031444fd46dce83e8a8bb4972f0256d6b3d280d3", size = 23696, upload-time = "2025-11-29T00:52:35.99Z" }, + { url = "https://files.pythonhosted.org/packages/5e/90/893d290def15f687c35806ffb2fd45c3f34b07c61bd0348220cb602c8c38/pybcj-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb4a52cd573f4359a89fd3a4a1d82c914f8b758a5c9f16cd5dd13fb8aa24436", size = 24067, upload-time = "2025-11-29T00:52:37.013Z" }, + { url = "https://files.pythonhosted.org/packages/1f/51/19a45c2cb92c722a214b8cfd9a39d1e144b5d1f76c7d92c128b479f4e405/pybcj-1.0.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6d9a26fa9e627eb2fbba0f5b376ab42246bebdaf38cf437e384a6b7e3d78e23", size = 51675, upload-time = "2025-11-29T00:52:38.18Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/3c9e2323b97bbbe295d4b0bb1eb24f0ddb81e2fe67181b9668ab502ccefe/pybcj-1.0.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47207b69997fdc39e91a66812477506267964284b7d45fed68876dd74323d44f", size = 50539, upload-time = "2025-11-29T00:52:39.469Z" }, + { url = "https://files.pythonhosted.org/packages/f6/87/625dbdbb575cbee728b736bffc8f14bb0a8ee6cc23874d5cb598c28a67a5/pybcj-1.0.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b33d6ef1de94720f4856e198bd2b8eca978015ed685aef4138755ba3910eb963", size = 49720, upload-time = "2025-11-29T00:52:40.822Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/61dab2d5078ce5a36402fee31ba9ee637755b1700aeec3c4fe39ebb11c39/pybcj-1.0.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:053c7cda499a8934151d0c915b6efce8e53fa6b47d162434a5b24afef7af5d17", size = 49213, upload-time = "2025-11-29T00:52:41.951Z" }, + { url = "https://files.pythonhosted.org/packages/9c/19/4b756dc90d51492c1c13439b3de5da00137bd068001d4d6293e20686ff60/pybcj-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:555e90270d665d94cd34d2e50b096f68dba6baf7035ae11ac65c2bc126f8cef7", size = 24900, upload-time = "2025-11-29T00:52:43.484Z" }, + { url = "https://files.pythonhosted.org/packages/e9/50/426be36573eeeab9e771aa41ab9824edbb285bc5035c226ba766deec0ef9/pybcj-1.0.7-cp311-cp311-win_arm64.whl", hash = "sha256:22bdb390da9a4e38b2191070a62b88ad52edc3f6e12fe7eea278217ccfdbc02c", size = 23394, upload-time = "2025-11-29T00:52:44.528Z" }, + { url = "https://files.pythonhosted.org/packages/f5/60/39b51114e3e740b61844448c3b61be146781a5c0ffabcd473a17ba7f4336/pybcj-1.0.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d39787b85678d2ab1c67e2f21dd2e71be851f08e5c9fe619c605877b57dd529d", size = 31858, upload-time = "2025-11-29T00:52:45.566Z" }, + { url = "https://files.pythonhosted.org/packages/ee/15/df4cd94bdf6a73c2b6ecf5e99dd9dcfe654215992a0114860ff1c94752b5/pybcj-1.0.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8cd5dd166093a1fb146fb78859aac0f00b45db6c11074705517bc72a940a1c8e", size = 23746, upload-time = "2025-11-29T00:52:46.947Z" }, + { url = "https://files.pythonhosted.org/packages/15/1a/f8bbe5f9ad95a0c2d1853006a93021aa1c2851b25a6bccc0894b1d72c0f4/pybcj-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82152e8641f5ce68638f3504227065f27b6b1efe96479ffbf20d81530c220062", size = 24088, upload-time = "2025-11-29T00:52:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/53675b56f9dbcb7a4dd681af8c05b1abf95fea3cdf7bf64872b9e0fdc8c8/pybcj-1.0.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2095b45d05f8d19430167b7df52ebd920df854ab8d064bae879df0a4611374b3", size = 52407, upload-time = "2025-11-29T00:52:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/a1/56/3008e748c7d35c407db97b77af52ade07756033250d0e208a6af231131ca/pybcj-1.0.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b400c9f48faed01edb7f0df54b4354270325c886e785f31c866c581a46023b", size = 51462, upload-time = "2025-11-29T00:52:51.016Z" }, + { url = "https://files.pythonhosted.org/packages/83/f4/8e8b079af7ac6a51b2edcb8bed6040a9748542cb1daf55387f769f9571d0/pybcj-1.0.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8210e51a2d4e5ccb4fdb75a1e692dd8c121858b589026bb28988ed7ffdb7ed00", size = 50320, upload-time = "2025-11-29T00:52:52.835Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/c2277eeb083029313f5822a491c39d7af91ebd1e717f42c772d56b8c3c4e/pybcj-1.0.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6c3fe420083186ae2e5f75c23aa6563dcb030b8fc188d00778ce374d1df1984", size = 49987, upload-time = "2025-11-29T00:52:53.904Z" }, + { url = "https://files.pythonhosted.org/packages/a0/52/711a94d5ae634ff3dd51324a40885158f819ba660b4601653bd78bbd33cd/pybcj-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:c435062d66364f85674a639541980000e37657b98367a2ce2699514e44b8ab05", size = 24940, upload-time = "2025-11-29T00:52:54.973Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d4/02e3ca25deca3359e63b70e5804bdbae9f400d03f93d0c66341e0471bb39/pybcj-1.0.7-cp312-cp312-win_arm64.whl", hash = "sha256:3f74fd70b08092e58b1ee13c67fbf9de63d73eb1c61ab06670a0d7161efeb252", size = 23410, upload-time = "2025-11-29T00:52:55.944Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6f/b08d5be15209b584858981b44a447ae7a6d8c591487e502e212b5420f94e/pybcj-1.0.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d5e0feeeee3a659b30d7afbd89bf41da84e8c8fe13e5b997457e799a70fa550", size = 31869, upload-time = "2025-11-29T00:52:56.973Z" }, + { url = "https://files.pythonhosted.org/packages/47/e5/68bffbc87581ea96bb4aea623d8cd085786f36d5b912ed8d9bade3265110/pybcj-1.0.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:60baaf9f0da31438515a401145f920f75f2ec7d511165bbf57475467af72a3e6", size = 23750, upload-time = "2025-11-29T00:52:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/718d6daa0d5e15e2131301220eaefbf6bc8cd0c90791c5ac18c893be111a/pybcj-1.0.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c6e726618c3d43c730df5a4067fc19653b360f89c2f72f4323dae10d324552", size = 24089, upload-time = "2025-11-29T00:52:59.457Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a6/7df6b55b64b370e2d42aa51b88402336d7f17c97eda284755f404d8d6047/pybcj-1.0.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a5fcd40a4ce8f0c5428032ec5db9f03abb42214b993886cdf558e5644de636e", size = 52454, upload-time = "2025-11-29T00:53:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/2ce282501a39f32f27cef2a3ee11c621f9d5348da4441d2326f2fcc9b17e/pybcj-1.0.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:029112255c22de66e0117bec932c8be341ed20c56dcf6a961c14689f7f0ce772", size = 51522, upload-time = "2025-11-29T00:53:01.714Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2a/f75ea5f09c91e01456a13759a325e007663855fa16af28830ce7d44d5427/pybcj-1.0.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6492bcef5cb6883506b9dce5e48cb81217407305957b0e602c6c689c60097c5e", size = 50398, upload-time = "2025-11-29T00:53:02.839Z" }, + { url = "https://files.pythonhosted.org/packages/5b/14/6d49e0c62a0ab68aa3325e6f141c33f37e5bc9d61cc9a1186c0a2d324fbe/pybcj-1.0.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22a7f4a51d36a1abb67a61e93248f997eb2be278f788d681096f5044ae18b4f9", size = 50087, upload-time = "2025-11-29T00:53:03.966Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/a0d58633236783028c08e585b3c4f4715a4286970b4ec4a25acfe23794f1/pybcj-1.0.7-cp313-cp313-win_amd64.whl", hash = "sha256:ebcce9b419fe5d3109150a1fab0fc93a64d5cd812ca44c5ddb7d4f7128ea369f", size = 24939, upload-time = "2025-11-29T00:53:05.363Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/8856cd8bc07e66322a55750ab87a264829cdfbc6cd85c5844340cf06bc53/pybcj-1.0.7-cp313-cp313-win_arm64.whl", hash = "sha256:bc6acf0320976b4e31bdc0e59b16689083d5c346a6c62ac4f799685d1cc5cf27", size = 23409, upload-time = "2025-11-29T00:53:06.749Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9e/eb50f11ea7fb6342167bb8353d48966b490afa8ae47c98917e9acd045b71/pybcj-1.0.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:293f951eb3877840acab79f0c4dcfc06eab03e087cb9e4c004ec058e093acb1d", size = 32409, upload-time = "2025-11-29T00:53:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f6/b55f4a5faf9bd162426f49de84873fbf813698d1b018234c3c99816a9662/pybcj-1.0.7-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3ae64960904f362d33ffca10715803afd9f9a6a2a592f871dcb335acf82edf29", size = 23818, upload-time = "2025-11-29T00:53:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/4c/24/78cf0973b3deded1e072479ac35d387083a626b3dc0b29e2e099cf73e082/pybcj-1.0.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:70aa4476910f982025f878e598c136559a6d78b59fc20ba8b4b592306cde6051", size = 24115, upload-time = "2025-11-29T00:53:10.241Z" }, + { url = "https://files.pythonhosted.org/packages/e6/69/e044d7127018158c67b119e6ed26d1a4b34b1e1defd9b0d3de029a782b9f/pybcj-1.0.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79ce3ce9b380b1b75c5e490abc3888ee3b5b2d28c22b59618674bf410b9cee16", size = 52484, upload-time = "2025-11-29T00:53:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/98/05/55337b0a9807887967b2cf49dd6f3bf47c7745786b0bda51a5cbfda66c78/pybcj-1.0.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc73ee1bc064d6f97dfd66051d3859b32e1b6a4cf89b077f5c8ef6c2dccb71af", size = 51471, upload-time = "2025-11-29T00:53:12.86Z" }, + { url = "https://files.pythonhosted.org/packages/59/09/6c57cbbf4931ac304f822ef78f478dd6b34b00b08e9530d308737959f064/pybcj-1.0.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5b0d13f41a9f85b3f95dd5dc7bfaa9539e80f8ae60a96db7f34c07ed732e4a82", size = 50435, upload-time = "2025-11-29T00:53:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/26415ca1b96456e27550dae67092023af3e8454621b1efa701079d8acb64/pybcj-1.0.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:597d7e9a8cbb30a6ed54d552fd3436edb32bbb821a7ac2fa8e5c7ebd1f7e0e93", size = 50010, upload-time = "2025-11-29T00:53:15.396Z" }, + { url = "https://files.pythonhosted.org/packages/91/f7/d4e55aede85143adff3ded89a1ae87bbf29060ddc2249fb861d78b103d4a/pybcj-1.0.7-cp314-cp314-win_amd64.whl", hash = "sha256:4603cc41ceb1236abe9169e2ead344140be5d2c3ac01bbc5e44cb1b13078a009", size = 25283, upload-time = "2025-11-29T00:53:16.472Z" }, + { url = "https://files.pythonhosted.org/packages/f6/19/9939ba437140d7375aecf8063d8695dfeea56ca40f57ddaa6ca3b828c131/pybcj-1.0.7-cp314-cp314-win_arm64.whl", hash = "sha256:adf985e816ddd59f3bf6d1066b7fa89de7424a4f19f3725f9976284cabe54e28", size = 23622, upload-time = "2025-11-29T00:53:17.994Z" }, + { url = "https://files.pythonhosted.org/packages/62/6e/5e14c70f3ddc268f28e7ac912510c8bc2b5430f18bb7f326bbb9d1878955/pybcj-1.0.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9bbd835873de147481d62c11ba91a75d26a72df1142de3516b384b04e5a1db6d", size = 33016, upload-time = "2025-11-29T00:53:19.434Z" }, + { url = "https://files.pythonhosted.org/packages/49/c1/25a1a8d5811c5d2a2ca3439c548abbba882fa72eba9a6eb41040206fdc26/pybcj-1.0.7-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b7576b25d7b01a953e2f987e77cef93c001db7b95924a5541d5a55f9195a7e89", size = 24114, upload-time = "2025-11-29T00:53:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/35/66/09567564cdc6cc2713d729bd29794367ebb67fa9ea5c10313c49608c08fa/pybcj-1.0.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57b36920498f82ca6a325a98b13e0fbff8fc29bade7aaaddc7d284640bffd87d", size = 24428, upload-time = "2025-11-29T00:53:21.522Z" }, + { url = "https://files.pythonhosted.org/packages/c5/49/4f6793624eb418cc2536ef39e67c8783d7ca542f6139a051e9d2d76fdc80/pybcj-1.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aac2a46faf41e373939f6d3e6a5aa2121bf09e2446972c14a8e5d1ca3b0f8130", size = 58806, upload-time = "2025-11-29T00:53:22.568Z" }, + { url = "https://files.pythonhosted.org/packages/be/1c/8604f7fe360a9340bbf798b826a88f8e9d186fc031eb531bcd11ac9e684b/pybcj-1.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c7d6156ef2b4e8ecd450b62dc4cc3a89e8dda307cb26288b670952ef0df3a37", size = 56975, upload-time = "2025-11-29T00:53:23.691Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/d706b4220f60fa4e6a5756dc0051fb76c32a5615958fc12f0dcacef3f86b/pybcj-1.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0fe306213de1e764abae63c06ae5a4e9a83632f62612805f1f883b8d74431901", size = 56023, upload-time = "2025-11-29T00:53:25.135Z" }, + { url = "https://files.pythonhosted.org/packages/3e/28/ad306f5acb1226241960bb86e9021b4e32bb7da426ccdc824d9d36d61261/pybcj-1.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00448182d535cca37e8f24d892d480fa86f80ff20c79385f6eca75f118efcbb4", size = 55194, upload-time = "2025-11-29T00:53:26.209Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d8/70c5e5701926fc67dae02101f0298d21c0b89ff83fa705712c3b4da252e5/pybcj-1.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:7e94aa712d0fa5fda9875828441755ece7121fc3f8c5cc3bc8ee92d05b853590", size = 25826, upload-time = "2025-11-29T00:53:27.655Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5d/7a87ba32c0c0756f36000fafe642fa4609be2c26a50a7913a057a47eabdf/pybcj-1.0.7-cp314-cp314t-win_arm64.whl", hash = "sha256:16fd4e51a5556d1f38d7ba5d1fab588bfb60ae23d2299b5179779bf9900adf71", size = 24049, upload-time = "2025-11-29T00:53:28.679Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodomex" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157, upload-time = "2025-05-17T17:23:41.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/00/10edb04777069a42490a38c137099d4b17ba6e36a4e6e28bdc7470e9e853/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886", size = 2498764, upload-time = "2025-05-17T17:22:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3f/2872a9c2d3a27eac094f9ceaa5a8a483b774ae69018040ea3240d5b11154/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d", size = 1643012, upload-time = "2025-05-17T17:22:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/774c2e2b4f6570fbf6a4972161adbb183aeeaa1863bde31e8706f123bf92/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa", size = 2187643, upload-time = "2025-05-17T17:22:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/de/a3/71065b24cb889d537954cedc3ae5466af00a2cabcff8e29b73be047e9a19/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8", size = 2273762, upload-time = "2025-05-17T17:22:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0b/ff6f43b7fbef4d302c8b981fe58467b8871902cdc3eb28896b52421422cc/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5", size = 2313012, upload-time = "2025-05-17T17:22:30.57Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/9d4772c0506ab6da10b41159493657105d3f8bb5c53615d19452afc6b315/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314", size = 2186856, upload-time = "2025-05-17T17:22:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/8b30efcd6341707a234e5eba5493700a17852ca1ac7a75daa7945fcf6427/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006", size = 2347523, upload-time = "2025-05-17T17:22:35.386Z" }, + { url = "https://files.pythonhosted.org/packages/0f/02/16868e9f655b7670dbb0ac4f2844145cbc42251f916fc35c414ad2359849/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462", size = 2272825, upload-time = "2025-05-17T17:22:37.632Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328", size = 1772078, upload-time = "2025-05-17T17:22:40Z" }, + { url = "https://files.pythonhosted.org/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708", size = 1803656, upload-time = "2025-05-17T17:22:42.139Z" }, + { url = "https://files.pythonhosted.org/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4", size = 1707172, upload-time = "2025-05-17T17:22:44.704Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240, upload-time = "2025-05-17T17:22:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042, upload-time = "2025-05-17T17:22:49.098Z" }, + { url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227, upload-time = "2025-05-17T17:22:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578, upload-time = "2025-05-17T17:22:53.676Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166, upload-time = "2025-05-17T17:22:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467, upload-time = "2025-05-17T17:22:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104, upload-time = "2025-05-17T17:23:02.112Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038, upload-time = "2025-05-17T17:23:04.872Z" }, + { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/3e76d948c3c4ac71335bbe75dac53e154b40b0f8f1f022dfa295257a0c96/pycryptodomex-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ebfff755c360d674306e5891c564a274a47953562b42fb74a5c25b8fc1fb1cb5", size = 1627695, upload-time = "2025-05-17T17:23:17.38Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/80f4297a4820dfdfd1c88cf6c4666a200f204b3488103d027b5edd9176ec/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca54f4bb349d45afc17e3011ed4264ef1cc9e266699874cdd1349c504e64798", size = 1675772, upload-time = "2025-05-17T17:23:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/d1/42/1e969ee0ad19fe3134b0e1b856c39bd0b70d47a4d0e81c2a8b05727394c9/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2596e643d4365e14d0879dc5aafe6355616c61c2176009270f3048f6d9a61f", size = 1668083, upload-time = "2025-05-17T17:23:21.867Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c3/1de4f7631fea8a992a44ba632aa40e0008764c0fb9bf2854b0acf78c2cf2/pycryptodomex-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdfac7cda115bca3a5abb2f9e43bc2fb66c2b65ab074913643803ca7083a79ea", size = 1706056, upload-time = "2025-05-17T17:23:24.031Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5f/af7da8e6f1e42b52f44a24d08b8e4c726207434e2593732d39e7af5e7256/pycryptodomex-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:14c37aaece158d0ace436f76a7bb19093db3b4deade9797abfc39ec6cd6cc2fe", size = 1806478, upload-time = "2025-05-17T17:23:26.066Z" }, +] + +[[package]] +name = "pyppmd" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/d7/803232913cab9163a1a97ecf2236cd7135903c46ac8d49613448d88e8759/pyppmd-1.3.1.tar.gz", hash = "sha256:ced527f08ade4408c1bfc5264e9f97ffac8d221c9d13eca4f35ec1ec0c7b6b2e", size = 1351815, upload-time = "2025-11-27T22:08:44.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/f2/175dbdc56178d5d00caa73df79f98f72fab80f5c01a7467aff3ed245f739/pyppmd-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:041f46fbeb0a59888c0a94d6b9a557c652935633a104be1c31c12de491b5f448", size = 77545, upload-time = "2025-11-27T22:07:22.938Z" }, + { url = "https://files.pythonhosted.org/packages/61/a0/729139273084dd574b3b529cd4b006ca9d2dc42d8bd3b65adf66ae8dffd8/pyppmd-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9512a8b39740923559c26eb16266bf8b70d4eab6ad27a9b39cd2465e60e0acfa", size = 47997, upload-time = "2025-11-27T22:07:24.437Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/eab338c9fa00ddeb3f54552360949ac503f3cd77debc71857c07263eba20/pyppmd-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8966f26b91ba7cdff3cfec5512d39d1f8bf4a8dbb75c44085e33b564566fea66", size = 48437, upload-time = "2025-11-27T22:07:25.558Z" }, + { url = "https://files.pythonhosted.org/packages/96/5b/f098d68ce887d25a47e95a1aaa6443436cb21bb4bcc3cd73f332135a214d/pyppmd-1.3.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d1cff657e85655c67426c29c90c78a6210148b207993e643fc351c72c60d188", size = 139198, upload-time = "2025-11-27T22:07:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1e/59757d7e3aba883b2e446baee07055a069c9e17143fd4bd297fe590dd461/pyppmd-1.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9de2cdcc3932e7c23a54beb48dfe1b5ab7b4aedd5ffaae1e4871bd213d630cb3", size = 140959, upload-time = "2025-11-27T22:07:28.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/4d1aeea9bcb8293dada21b51e2d20eff49d24ddf512ec611b47d2440846a/pyppmd-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e1985461219c30d4576070b7e2de718dbb6f32637d1e658d25f838dfda2a4bb", size = 136301, upload-time = "2025-11-27T22:07:30.523Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/f6a46a60b092b5fb9a9631be408e447ea87cce4d656197736586ba082975/pyppmd-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20d9d1aa4d0f32118c8094c212c66b7af50e55f47e7c6dffa5f35a8ac391faca", size = 139126, upload-time = "2025-11-27T22:07:32.274Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ed/cf1f4ac5248ddcb19cf18ef8acb256d909912a5c5684fcfe400fb42d778d/pyppmd-1.3.1-cp310-cp310-win32.whl", hash = "sha256:44d25e7dede2abb614bc023fe87835365fdd5865981c2273b70bfad71b84db29", size = 42019, upload-time = "2025-11-27T22:07:33.442Z" }, + { url = "https://files.pythonhosted.org/packages/1b/93/1e035bd1ef405ca024be1fc4d9eb7d603eb03960d7010515ddf155dcccd2/pyppmd-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:1e503a28c9a275d31f24af9b735d2cca543b62f438b064e2833e9833e758bdbc", size = 46860, upload-time = "2025-11-27T22:07:34.873Z" }, + { url = "https://files.pythonhosted.org/packages/23/2f/236f7bf2f8b8ccfd93f0505692b9ad10a249fa00ce4671c50049c69ea414/pyppmd-1.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:3fb3708d7b2b38e2999385a2f02c8e68e0f5a364d94f94e475e2e8b09e9338fc", size = 45253, upload-time = "2025-11-27T22:07:36.406Z" }, + { url = "https://files.pythonhosted.org/packages/04/40/580b99818c9db3a09cbae8e3740f9b5127ef7fd7314daf009c487ab02eb6/pyppmd-1.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdf55aa6ee7aef492f6896464e7a5a528f8615bb9e435f55bc8dff226fcc8292", size = 77550, upload-time = "2025-11-27T22:07:37.839Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a3/f7b4c49ef920f5b2c9812f8f54f48e01435433cb6b46ec82b1b4e740714d/pyppmd-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa820aac385ac4ee57160b26d92862c69d31c08f92272dbef05fe8e619cea8d1", size = 48000, upload-time = "2025-11-27T22:07:38.983Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c3/30de6f7892e77b83350e55d54a6913420591cc2c5c4d2394c219a4c461ac/pyppmd-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e16593ba4ca0a85821ae698ef06847a52937662f5ce1b130c39cca2979a4e8cd", size = 48439, upload-time = "2025-11-27T22:07:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/39/22/33889cbe22a617c42df2199be54e9e1bf88393fd8643fe911eb9f982849c/pyppmd-1.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:486dde2294ff9b30465ab5bb0f213b20bd5ac0e4adf21be801a1ceb29aa75d9d", size = 141737, upload-time = "2025-11-27T22:07:41.641Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/940ab2fd78caaef3a9e9833adb36e17b9d03016d8b45ae110cbb4bfc83d1/pyppmd-1.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89730cf026416ae2546c92738966ecf117c8176d52c229ad621a61c34643818b", size = 143643, upload-time = "2025-11-27T22:07:42.892Z" }, + { url = "https://files.pythonhosted.org/packages/75/51/129ce02d2b28a4f6788bccfe6d0ae1defc45db10eb1c4e4389cde8994ec7/pyppmd-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e77f5a6770950d464b50da760d53e67bce308a3abc8e3bd51db620b3f8cf1fa8", size = 138625, upload-time = "2025-11-27T22:07:44.54Z" }, + { url = "https://files.pythonhosted.org/packages/3a/89/f8390d1a21951d3d4619c854a839f7b1431746997cd98b475f614a658305/pyppmd-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8e3bf8deef44f8e03612689a6067a4a3dd7e50d2ef00af4cf987c59b62ff3006", size = 141663, upload-time = "2025-11-27T22:07:45.814Z" }, + { url = "https://files.pythonhosted.org/packages/fa/3f/b58dd8875ebbb78436f87567547967bebdf3a5586f5f243c735fc4460843/pyppmd-1.3.1-cp311-cp311-win32.whl", hash = "sha256:a3509b3f881409ebc5522942438108c48a78f8df88bcf3f9d907b74131b9431c", size = 42014, upload-time = "2025-11-27T22:07:47.058Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d3/5fe05bdccdd7af04461c25a3dc34799f48ad3e9a2bd591aaec9afbc1d10a/pyppmd-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:23b83799f33f9a24577f22e092b0feecda8cd1ea33871ad8610a58629874f7bc", size = 46862, upload-time = "2025-11-27T22:07:48.162Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ba/6414cbe8407c23bacce906dad0599f155b2baf6082da104e64d7f1573717/pyppmd-1.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:234489036a1758670655d1ceafd4caeb93b858bd4c0ca39686837d38aef044c0", size = 45249, upload-time = "2025-11-27T22:07:49.261Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/696046e53c7aea98bb563aed3f15c3e2fa20c33e3d6c9de3c20992c586cc/pyppmd-1.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3faa58ab2ebe3b13ec23b1904639d687fb727270d2962fd2d239ca00fd6eb865", size = 77796, upload-time = "2025-11-27T22:07:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/a708cdb58e76889bc65201eda12211486548ffe00ecddc5dfbdce6d4d252/pyppmd-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27703f041ee96912a5410fd3ce31c5cde32f9323bd67f72f100bd960ee67bf13", size = 48185, upload-time = "2025-11-27T22:07:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/f299535c18009addb866d89a7044f1086e4eecf50542e57ff5bb15840195/pyppmd-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e773d8353b36f7e7973a43526993fb276b98a97839cb5dc8f4e6465ad873f41a", size = 48496, upload-time = "2025-11-27T22:07:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9d/4a59b73ea8e305f9192ee26ceb7c3d57e17ccb9bcca0e99ef335db29fcf7/pyppmd-1.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37b1883accf840cb0b711785d353f8548853a1401d381da007c0aec362f3ffac", size = 142620, upload-time = "2025-11-27T22:07:54.411Z" }, + { url = "https://files.pythonhosted.org/packages/88/d7/fe32c2a4f8539365e6292aed25545830a5e718a510cdb4caddd6fd8d8056/pyppmd-1.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bd6d179ad39b6191ca0cbe62fb9592f33f49277b4384ad7bc5eb0e6ca27ebee", size = 144306, upload-time = "2025-11-27T22:07:55.727Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/e3907cf263b58f4ed4c5315bc1ac91721c85332fd0d73a89e3e2752904ef/pyppmd-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:806cf8d33606e44bf5ff5786c57891f57993f1eef1c763da3c58ea97de3a13c8", size = 139522, upload-time = "2025-11-27T22:07:57.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/22d5f93251e481f23bf741dde7f59cfc3e315f60b32b63f215a2d7bb8944/pyppmd-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1826cbce9a2c944aa08df79310a7e6d4a61fd20636b6dff64a77ea4bc43da30f", size = 142340, upload-time = "2025-11-27T22:07:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/34/88/38d36c1394d5e331db9708dae08863c87c059bd5b6e43b20234be02a0503/pyppmd-1.3.1-cp312-cp312-win32.whl", hash = "sha256:d3ff96671319318d941dd34300d641745048e8a3251b077bddf98652d6ddc513", size = 42102, upload-time = "2025-11-27T22:08:00.124Z" }, + { url = "https://files.pythonhosted.org/packages/ee/29/1398c6f67dfb66367babeed2980caee837d0a488705e3dff7ff159e017e7/pyppmd-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:c8c1ad39e7ebde71bf5a54cf61f489bf4790f1dd0beb70dc2e8f5ad3329d7ca7", size = 46957, upload-time = "2025-11-27T22:08:01.587Z" }, + { url = "https://files.pythonhosted.org/packages/d2/41/ee3193c16472a5a1c5f1965abb8fa87a7ad455c39688e9a0c2d87d6a7027/pyppmd-1.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:391b2bf76d7dc45b343781754d0b734dcbf539b92667986a343f5488c4bf9ca0", size = 45214, upload-time = "2025-11-27T22:08:02.721Z" }, + { url = "https://files.pythonhosted.org/packages/f0/01/7cc3854b0e1304b90d2435e946db5e1c42e103adf840a68400002fc838b4/pyppmd-1.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4b4edb3e9619fd0bc39c1a07eb03e8731db833a93b23134f36c7ef581a94b37a", size = 77800, upload-time = "2025-11-27T22:08:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/caf6305224b9432ea7373670d101392f620c1ce741d6f95458b1fb9a16a4/pyppmd-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8b5c813e462c91048b88e2adfbcc0c69f2c905f70097001d32066f86f675bd4", size = 48195, upload-time = "2025-11-27T22:08:05.323Z" }, + { url = "https://files.pythonhosted.org/packages/45/82/8c5d3f0f738a3cf5c209d1471efda105a16417c0aaa91a2ad39efb3d4efb/pyppmd-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e8d372d9fac382183e0371cf0c2d736b494b1857a1befe98d563342b1205265b", size = 48482, upload-time = "2025-11-27T22:08:06.441Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/09ab8b4f00473f210284dacf837cae8355ad900f3fd8fd98b2bba12de4fa/pyppmd-1.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b765ae21f7ed2f4ea8f32bfd9e3a4a8d738e73fc8f8dcddec9cbe2c898d60be", size = 142828, upload-time = "2025-11-27T22:08:07.611Z" }, + { url = "https://files.pythonhosted.org/packages/64/fd/673d429df719affa1445611288aab6111c0bc87e51eb4677fe2421a1dd64/pyppmd-1.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd00522ddfcc292304577386b6c217758c0c10e1fb9ce7877ad7d3b7b821a808", size = 144502, upload-time = "2025-11-27T22:08:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/cf/0f/64dad213f56eaf23210ac7d3a5dedb19ad8f186f8b8682f65e7d51d9fa4a/pyppmd-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3de62099ff2ca876c2d39bc547bcba6f7b878988663abd782a5bad4edac3bb44", size = 139718, upload-time = "2025-11-27T22:08:10.114Z" }, + { url = "https://files.pythonhosted.org/packages/74/61/bd5dbe8d7374748687d48f0bc44e6b930470e09552b1bbc65ef899ab673e/pyppmd-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:011f845de195d60fe973a635a1f4be981b7d80f357a8acb1b2d83bdf5087c808", size = 142578, upload-time = "2025-11-27T22:08:11.388Z" }, + { url = "https://files.pythonhosted.org/packages/42/ae/0af00a9b1cd59b6821eb182413033d5ddbe05dfcde7511e3a6b1a5b83f6d/pyppmd-1.3.1-cp313-cp313-win32.whl", hash = "sha256:7d61bd01f25289b6ae54832db4254602fb0c6d105f6e6bf0aee39b803b698b98", size = 42102, upload-time = "2025-11-27T22:08:13.297Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6d/311d8faca142f39b4d158c94ed7eb237a197d41dd7eb67f53442a7904e04/pyppmd-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:df8d84ab72381058a964ba66e5e81ed52dbd0b5ad734a5ef8353452983506098", size = 46962, upload-time = "2025-11-27T22:08:14.759Z" }, + { url = "https://files.pythonhosted.org/packages/bb/42/24b59b6bb73a3fec96c7f81521d146c2ac89ffcb89cf9e848f62e0660381/pyppmd-1.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:124a04aab6936ba011f9ad57067798c7f052fdb1848b0cc4318606eea55475e6", size = 45209, upload-time = "2025-11-27T22:08:15.933Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/ac2a011af1c7c40b6482e60d85d267ac5923fb8794b51bfddbb56b04d1ec/pyppmd-1.3.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ea53f71ac16e113599b8441a9d8b6dcd71cfdf15cdb33ba5151810b8e656c5ec", size = 77897, upload-time = "2025-11-27T22:08:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/20/13/737f4dac06685865d23f51b7061dbe9373c7bcc60b5b6456cd08301bc807/pyppmd-1.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:985c8703b53e5f68fe17f653e96748d60b1f855676c852a6e67cd472eb853671", size = 48268, upload-time = "2025-11-27T22:08:18.263Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d3/c50ebaee8b8a064fe9a534e98df5051e309efb705728aadff4e6893cd87f/pyppmd-1.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:33daa996ad5203c665c0b55aff6329817b2cb7fa95f2c33a2e83ed0121b400cb", size = 48521, upload-time = "2025-11-27T22:08:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/80/6c/4a97c0a0ab7640aeddde4843cb6381b80ecb71fe2c6c48b9032d60586b11/pyppmd-1.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b49870c6d7194f6eb80f30335ca03596d153e02fcde2c222e4f1202ac25f7fcf", size = 142892, upload-time = "2025-11-27T22:08:20.972Z" }, + { url = "https://files.pythonhosted.org/packages/8f/22/744c32c7da3de171d9e62a1b2cb4104544ac778358565a3dbc06a2253324/pyppmd-1.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:385e92c97c42e8a6f0bfc0e4acfc6c074cb1ba3a2f650f292696dd9f19e2e603", size = 144550, upload-time = "2025-11-27T22:08:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/84af6808e0754923062122d3ee9f0532050f1612069d558bb5d53978e67a/pyppmd-1.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:017a1e2903f1c3147a1046db486990d401e8a25eb52c320b1fc2fb3e7b83cbeb", size = 139850, upload-time = "2025-11-27T22:08:23.863Z" }, + { url = "https://files.pythonhosted.org/packages/20/5e/dc6166ea7929d442625301289d99313a5b358e3cb2e927a6bd913047e8ee/pyppmd-1.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f2770a4b777c0c5236b3d9294b7bf4bc15538c95d45b2079eb8ebc1298e62e37", size = 142589, upload-time = "2025-11-27T22:08:25.228Z" }, + { url = "https://files.pythonhosted.org/packages/77/92/adc6b12ddf11173a02fd631010f88ad4fe4c532363959638d5d53583a3d9/pyppmd-1.3.1-cp314-cp314-win32.whl", hash = "sha256:b9d54cd59ce97f2ba57be1da91b3d874d129faca21c9565d7afec111f942e6a1", size = 42761, upload-time = "2025-11-27T22:08:26.917Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/5003a413d9f2743298a070df6713a75dedccc470e7adeeced5b43cad7418/pyppmd-1.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0e64247618cb150d2909beb0137da3084fef1d3479b4cc73b5b47fda7611abf9", size = 47806, upload-time = "2025-11-27T22:08:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/07/b7/db927e1df7fc3132cb57960f4ea23bf174c7e86ccec22857e6a35cccdae7/pyppmd-1.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:d354d6e551d2630b0ac98f27e3ad63e86cdcac9ce2115b5dfe46e2c9d3f4e82c", size = 46122, upload-time = "2025-11-27T22:08:29.191Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/3150227624f374b06e331a7f67ae3383e796dd90973ee17ec3e35d30524c/pyppmd-1.3.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2d77ed79662c32e2551748d59763cfe3dcd10855bf3495937e3d5e5917507818", size = 78876, upload-time = "2025-11-27T22:08:30.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/11a3bba62d1d92050f4dd86db810062b506ec76d20b6e00b4ab567ff15e4/pyppmd-1.3.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6a1f92b94635c23d85270bb26db25cc0db544e436af86efc1cf58302d71d5af1", size = 48830, upload-time = "2025-11-27T22:08:32.087Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/e8f50cf89c689543d5a16c7be3df3c4afd43cbeb1a019b84ff9e82f13415/pyppmd-1.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:610f214f2405e27eb5a3dad7fa15f385cfc42141a01cda71995d9c1e0b09fab9", size = 48956, upload-time = "2025-11-27T22:08:33.567Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0f/944b30679a8dea3ea95a66531ee30cb6feb5d011ec7fd6832069d9130bf3/pyppmd-1.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae81a14895498d9a23429d92114c98da478b74b8e33251527d7cff3e01c09de0", size = 152872, upload-time = "2025-11-27T22:08:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/ae/87/1e48ea92994f4c72dc9b5520a6a386b21c524d3d2969c2c2a5c325929ad7/pyppmd-1.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1683e6d1ba09e377e0ae02de3a518191a3d63ccdb0b6037c74e6ddf577b5644", size = 154210, upload-time = "2025-11-27T22:08:36.368Z" }, + { url = "https://files.pythonhosted.org/packages/3f/21/650911f98c4cb360442724bbdade27cb3679b0587ea77e73512693d2918d/pyppmd-1.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e4d74fa0f3531e9dadc56e0ace41bce82d3c0babed47b3f224101dc0dbde7287", size = 147636, upload-time = "2025-11-27T22:08:37.657Z" }, + { url = "https://files.pythonhosted.org/packages/7c/29/4f8cfc1dd67b04ced8c10669fa14c4e35a42cf4a84e1101793735a82d5f0/pyppmd-1.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f8d6375b18b9c79127fee0885cfd52e2e983edb67041464309426571d38dcd4", size = 150998, upload-time = "2025-11-27T22:08:38.951Z" }, + { url = "https://files.pythonhosted.org/packages/c4/da/dc9e5e10bc56056bd8c33e533517cb327752cbaf172688bba3c23f6b2318/pyppmd-1.3.1-cp314-cp314t-win32.whl", hash = "sha256:de87f7acd575fb07a4ff42d41bcc071570fe759a36f345f1f54f574ecfccfc5b", size = 43016, upload-time = "2025-11-27T22:08:40.234Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a0/46dc15549ad2c0427a9825730e6bdf342045ad410543368afd4389a16f36/pyppmd-1.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:76e4800aa67292b4cc80058fd29b39e02a5dded721af9fe5654f356ef24307f4", size = 48636, upload-time = "2025-11-27T22:08:41.45Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7b/7e9a83848de928c26f0ab3ee105c0da63a932a0804a94807205e6313e73a/pyppmd-1.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:e066cbf1d335fe20480cd8ecc10848ba78d99fe6d1e44ea00def48feaf46afdf", size = 46402, upload-time = "2025-11-27T22:08:42.804Z" }, +] + [[package]] name = "sbackup" version = "1.0.0" source = { virtual = "." } dependencies = [ + { name = "py7zr" }, { name = "tqdm" }, + { name = "zstandard" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, ] [package.metadata] -requires-dist = [{ name = "tqdm", specifier = ">=4.67.3" }] +requires-dist = [ + { name = "py7zr", specifier = ">=1.1.0" }, + { name = "tqdm", specifier = ">=4.67.3" }, + { name = "zstandard", specifier = ">=0.25.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "coverage", specifier = ">=7.13.5" }] + +[[package]] +name = "texttable" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/dc/0aff23d6036a4d3bf4f1d8c8204c5c79c4437e25e0ae94ffe4bbb55ee3c2/texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638", size = 12831, upload-time = "2023-10-03T09:48:12.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/99/4772b8e00a136f3e01236de33b0efda31ee7077203ba5967fcc76da94d65/texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917", size = 10768, upload-time = "2023-10-03T09:48:10.434Z" }, +] [[package]] name = "tqdm" @@ -33,3 +743,93 @@ sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd wheels = [ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]