Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d982509acb | ||
| e66bdc9a8f | |||
| 598a71dcc8 | |||
| 385d721155 | |||
| 1cb0bca865 | |||
|
|
6dbadc476a | ||
| 5f9d8a81d4 | |||
| 60ff38e207 | |||
|
|
19f285db9c | ||
| 8cf2a5f1ac | |||
|
|
7cb2c3adb2 | ||
| a0d6acfa8a | |||
| 79473eb05a | |||
| fde29fe90d | |||
| a068c0a5bd | |||
| 27b9039ddd | |||
|
|
e347b12d94 | ||
| e213edf7b2 | |||
|
|
695a0d33e1 | ||
| 2b24d252a1 | |||
|
|
7e989355b9 | ||
| 59a3789ff5 | |||
| 0fe9045f54 | |||
| 80129d6bc5 | |||
| 863b1b3a21 | |||
| b829faf658 | |||
| 16ad34c953 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @mdaleo404
|
||||
29
.github/workflows/lint-and-security.yml
vendored
Normal file
29
.github/workflows/lint-and-security.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Lint & Security
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
precommit-and-security:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit
|
||||
|
||||
- name: Run pre-commit hooks
|
||||
uses: pre-commit/action@v3.0.1
|
||||
|
||||
- name: Install pip-audit
|
||||
run: pip install pip-audit
|
||||
|
||||
- name: Run pip-audit
|
||||
run: pip-audit
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
__pycache__
|
||||
.pytest_cache
|
||||
dist
|
||||
dist
|
||||
.coverage
|
||||
|
||||
21
.pre-commit-config.yaml
Normal file
21
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
repos:
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.9
|
||||
hooks:
|
||||
- id: bandit
|
||||
files: ^src/mirro/
|
||||
args: ["-lll", "-iii", "-s", "B110,B112"]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 25.11.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.13
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
113
README.md
113
README.md
@@ -6,7 +6,7 @@ You edit a temporary file, **mirro** detects whether anything changed, and if it
|
||||
|
||||
## Why mirro?
|
||||
|
||||
Well... have you ever been in the _“ugh, I forgot to back this up first”_ situation?
|
||||
Well... have you ever been in the _“ugh, I forgot to back this up first”_ situation?
|
||||
|
||||
No?
|
||||
|
||||
@@ -26,6 +26,8 @@ Stop lying... 🥸
|
||||
|
||||
- requires `sudo` only when actually needed
|
||||
|
||||
- accepts most of your favourite editor's flags
|
||||
|
||||
It’s simple, predictable, and hard to misuse.
|
||||
|
||||
I mean... the only thing you need to remember is _to use it_.
|
||||
@@ -74,22 +76,121 @@ so under `sudo`:
|
||||
|
||||
Backups are named like:
|
||||
```
|
||||
filename.ext.orig.20251110T174400.bak
|
||||
filename.ext.orig.20251110T174400
|
||||
```
|
||||
|
||||
## Functionalities
|
||||
|
||||
### List all backup files stored in your backup directory.
|
||||
```
|
||||
mirro --list
|
||||
```
|
||||
Output includes permissions, owner/group, timestamps, and backup filenames.
|
||||
|
||||
### Restore the most recent backup for a given file.
|
||||
```
|
||||
mirro --restore-last ~/.config/myapp/config.ini
|
||||
```
|
||||
This:
|
||||
1. finds the newest backup matching the filename,
|
||||
|
||||
2. strips the mirro header from it,
|
||||
|
||||
3. and overwrites the target file with its original contents.
|
||||
|
||||
### Remove old backup files.
|
||||
```
|
||||
mirro --prune-backups
|
||||
```
|
||||
This removes backups older than the number of days set in `MIRRO_BACKUPS_LIFE`.
|
||||
|
||||
### Remove backups older than _N_ days
|
||||
```
|
||||
mirro --prune-backups=14
|
||||
```
|
||||
This keeps the last 14 days of backups and removes everything older.
|
||||
|
||||
### Remove all backups
|
||||
```
|
||||
mirro --prune-backups=all
|
||||
```
|
||||
This deletes every backup in the backup directory.
|
||||
|
||||
### Environment Variable
|
||||
`MIRRO_BACKUPS_LIFE` controls the default number of days to keep when using `mirro --prune-backups`.
|
||||
Its default value is **30** if not set otherwise.
|
||||
```
|
||||
export MIRRO_BACKUPS_LIFE=7
|
||||
```
|
||||
Backups older than 7 days will be removed.
|
||||
|
||||
Invalid or non-numeric values fall back to 30 days.
|
||||
|
||||
**Note:** _a value of 0 is **invalid**_.
|
||||
|
||||
### Built-in diff
|
||||
This shows a _git-like_ diff of the current file version and any of that file backups.
|
||||
```
|
||||
mirro --diff file file.orig.20251121T163121
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
**NOTE**: To use mirro with sudo, the path to mirro must be in the $PATH seen by root.
|
||||
Either install mirro as root (preferred), use sudo -E mirro, or add the $PATH to /etc/sudoers using its Defaults secure_path parameter.
|
||||
### From package manager
|
||||
|
||||
Install via PyPI (preferred):
|
||||
This is the preferred method of installation.
|
||||
|
||||
**Ubuntu 22.04 and 24.04**
|
||||
```
|
||||
sudo add-apt-repository ppa:mdaleo/mirro
|
||||
sudo apt update
|
||||
sudo apt install mirro
|
||||
```
|
||||
|
||||
**Fedora 41, 42, 43**
|
||||
```
|
||||
sudo dnf copr enable mdaleo/mirro
|
||||
sudo dnf install resrm
|
||||
```
|
||||
|
||||
### From PyPI
|
||||
|
||||
**NOTE:** To use `mirro` with `sudo`, the path to `mirro` must be in the `$PATH` seen by `root`.\
|
||||
Either:
|
||||
|
||||
* install `mirro` as `root`, or
|
||||
* add the path to `mirro` to the `secure_path` parameter in `/etc/sudoers`. For example, where `/home/user/.local/bin` is where `mirro` is:
|
||||
|
||||
``` bash
|
||||
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/user/.local/bin"
|
||||
```
|
||||
|
||||
Install with:
|
||||
```
|
||||
pip install mirro
|
||||
```
|
||||
|
||||
Or clone the repo and install locally:
|
||||
### From this repository
|
||||
```
|
||||
git clone https://github.com/mdaleo404/mirro.git
|
||||
cd mirro/
|
||||
poetry install
|
||||
```
|
||||
|
||||
## How to run the tests
|
||||
|
||||
- Clone this repository
|
||||
|
||||
- Ensure you have Poetry installed
|
||||
|
||||
- Run `poetry run pytest -vvvv --cov=mirro --cov-report=term-missing --disable-warnings`
|
||||
|
||||
## pre-commit
|
||||
This project uses [**pre-commit**](https://pre-commit.com/) to run automatic formatting and security checks before each commit (Black, Bandit, and various safety checks).
|
||||
|
||||
To enable it:
|
||||
```
|
||||
poetry install
|
||||
poetry run pre-commit install
|
||||
```
|
||||
This ensures consistent formatting, catches common issues early, and keeps the codebase clean.
|
||||
|
||||
490
poetry.lock
generated
490
poetry.lock
generated
@@ -1,7 +1,491 @@
|
||||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
package = []
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.4.0"
|
||||
description = "Validate configuration and produce human readable error messages."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
|
||||
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.11.3"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "coverage-7.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c986537abca9b064510f3fd104ba33e98d3036608c7f2f5537f869bc10e1ee5"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28c5251b3ab1d23e66f1130ca0c419747edfbcb4690de19467cd616861507af7"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4f2bb4ee8dd40f9b2a80bb4adb2aecece9480ba1fa60d9382e8c8e0bd558e2eb"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e5f4bfac975a2138215a38bda599ef00162e4143541cf7dd186da10a7f8e69f1"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4cbfff5cf01fa07464439a8510affc9df281535f41a1f5312fbd2b59b4ab5c"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31663572f20bf3406d7ac00d6981c7bbbcec302539d26b5ac596ca499664de31"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9799bd6a910961cb666196b8583ed0ee125fa225c6fdee2cbf00232b861f29d2"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:097acc18bedf2c6e3144eaf09b5f6034926c3c9bb9e10574ffd0942717232507"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6f033dec603eea88204589175782290a038b436105a8f3637a81c4359df27832"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd9ca2d44ed8018c90efb72f237a2a140325a4c3339971364d758e78b175f58e"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-win32.whl", hash = "sha256:900580bc99c145e2561ea91a2d207e639171870d8a18756eb57db944a017d4bb"},
|
||||
{file = "coverage-7.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:c8be5bfcdc7832011b2652db29ed7672ce9d353dd19bce5272ca33dbcf60aaa8"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:200bb89fd2a8a07780eafcdff6463104dec459f3c838d980455cfa84f5e5e6e1"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d264402fc179776d43e557e1ca4a7d953020d3ee95f7ec19cc2c9d769277f06"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:385977d94fc155f8731c895accdfcc3dd0d9dd9ef90d102969df95d3c637ab80"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0542ddf6107adbd2592f29da9f59f5d9cff7947b5bb4f734805085c327dcffaa"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60bf4d7f886989ddf80e121a7f4d140d9eac91f1d2385ce8eb6bda93d563297"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0a3b6e32457535df0d41d2d895da46434706dd85dbaf53fbc0d3bd7d914b362"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:876a3ee7fd2613eb79602e4cdb39deb6b28c186e76124c3f29e580099ec21a87"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a730cd0824e8083989f304e97b3f884189efb48e2151e07f57e9e138ab104200"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b5cd111d3ab7390be0c07ad839235d5ad54d2ca497b5f5db86896098a77180a4"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:074e6a5cd38e06671580b4d872c1a67955d4e69639e4b04e87fc03b494c1f060"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-win32.whl", hash = "sha256:86d27d2dd7c7c5a44710565933c7dc9cd70e65ef97142e260d16d555667deef7"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:ca90ef33a152205fb6f2f0c1f3e55c50df4ef049bb0940ebba666edd4cdebc55"},
|
||||
{file = "coverage-7.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:56f909a40d68947ef726ce6a34eb38f0ed241ffbe55c5007c64e616663bcbafc"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b771b59ac0dfb7f139f70c85b42717ef400a6790abb6475ebac1ecee8de782f"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:603c4414125fc9ae9000f17912dcfd3d3eb677d4e360b85206539240c96ea76e"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:77ffb3b7704eb7b9b3298a01fe4509cef70117a52d50bcba29cffc5f53dd326a"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d4ca49f5ba432b0755ebb0fc3a56be944a19a16bb33802264bbc7311622c0d1"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05fd3fb6edff0c98874d752013588836f458261e5eba587afe4c547bba544afd"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e920567f8c3a3ce68ae5a42cf7c2dc4bb6cc389f18bff2235dd8c03fa405de5"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bec8c7160688bd5a34e65c82984b25409563134d63285d8943d0599efbc448e"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:adb9b7b42c802bd8cb3927de8c1c26368ce50c8fdaa83a9d8551384d77537044"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c8f563b245b4ddb591e99f28e3cd140b85f114b38b7f95b2e42542f0603eb7d7"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2a96fdc7643c9517a317553aca13b5cae9bad9a5f32f4654ce247ae4d321405"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-win32.whl", hash = "sha256:e8feeb5e8705835f0622af0fe7ff8d5cb388948454647086494d6c41ec142c2e"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:abb903ffe46bd319d99979cdba350ae7016759bb69f47882242f7b93f3356055"},
|
||||
{file = "coverage-7.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:1451464fd855d9bd000c19b71bb7dafea9ab815741fb0bd9e813d9b671462d6f"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-win32.whl", hash = "sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2"},
|
||||
{file = "coverage-7.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-win32.whl", hash = "sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac"},
|
||||
{file = "coverage-7.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fdc5255eb4815babcdf236fa1a806ccb546724c8a9b129fd1ea4a5448a0bf07c"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fe3425dc6021f906c6325d3c415e048e7cdb955505a94f1eb774dafc779ba203"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4ca5f876bf41b24378ee67c41d688155f0e54cdc720de8ef9ad6544005899240"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9061a3e3c92b27fd8036dafa26f25d95695b6aa2e4514ab16a254f297e664f83"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abcea3b5f0dc44e1d01c27090bc32ce6ffb7aa665f884f1890710454113ea902"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:68c4eb92997dbaaf839ea13527be463178ac0ddd37a7ac636b8bc11a51af2428"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:149eccc85d48c8f06547534068c41d69a1a35322deaa4d69ba1561e2e9127e75"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:08c0bcf932e47795c49f0406054824b9d45671362dfc4269e0bc6e4bff010704"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:39764c6167c82d68a2d8c97c33dba45ec0ad9172570860e12191416f4f8e6e1b"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3224c7baf34e923ffc78cb45e793925539d640d42c96646db62dbd61bbcfa131"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-win32.whl", hash = "sha256:c713c1c528284d636cd37723b0b4c35c11190da6f932794e145fc40f8210a14a"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:c381a252317f63ca0179d2c7918e83b99a4ff3101e1b24849b999a00f9cd4f86"},
|
||||
{file = "coverage-7.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:3e33a968672be1394eded257ec10d4acbb9af2ae263ba05a99ff901bb863557e"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f9c96a29c6d65bd36a91f5634fef800212dff69dacdb44345c4c9783943ab0df"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ec27a7a991d229213c8070d31e3ecf44d005d96a9edc30c78eaeafaa421c001"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:72c8b494bd20ae1c58528b97c4a67d5cfeafcb3845c73542875ecd43924296de"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:60ca149a446da255d56c2a7a813b51a80d9497a62250532598d249b3cdb1a926"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5069074db19a534de3859c43eec78e962d6d119f637c41c8e028c5ab3f59dd"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac5d5329c9c942bbe6295f4251b135d860ed9f86acd912d418dce186de7c19ac"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e22539b676fafba17f0a90ac725f029a309eb6e483f364c86dcadee060429d46"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2376e8a9c889016f25472c452389e98bc6e54a19570b107e27cde9d47f387b64"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4234914b8c67238a3c4af2bba648dc716aa029ca44d01f3d51536d44ac16854f"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0b4101e2b3c6c352ff1f70b3a6fcc7c17c1ab1a91ccb7a33013cb0782af9820"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-win32.whl", hash = "sha256:305716afb19133762e8cf62745c46c4853ad6f9eeba54a593e373289e24ea237"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9245bd392572b9f799261c4c9e7216bafc9405537d0f4ce3ad93afe081a12dc9"},
|
||||
{file = "coverage-7.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:9a1d577c20b4334e5e814c3d5fe07fa4a8c3ae42a601945e8d7940bab811d0bd"},
|
||||
{file = "coverage-7.11.3-py3-none-any.whl", hash = "sha256:351511ae28e2509c8d8cae5311577ea7dd511ab8e746ffc8814a0896c3d33fbe"},
|
||||
{file = "coverage-7.11.3.tar.gz", hash = "sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.0"
|
||||
description = "Distribution utilities"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"},
|
||||
{file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"},
|
||||
{file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.0"
|
||||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2"},
|
||||
{file = "filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.15"
|
||||
description = "File identification library for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"},
|
||||
{file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
license = ["ukkonen"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
|
||||
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
description = "Node.js virtual environment builder"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
|
||||
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
|
||||
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.5.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"},
|
||||
{file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"]
|
||||
type = ["mypy (>=1.18.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
|
||||
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["coverage", "pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "3.8.0"
|
||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
|
||||
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cfgv = ">=2.0.0"
|
||||
identify = ">=1.0.0"
|
||||
nodeenv = ">=0.11.1"
|
||||
pyyaml = ">=5.1"
|
||||
virtualenv = ">=20.10.0"
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
|
||||
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.1"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad"},
|
||||
{file = "pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
iniconfig = ">=1.0.1"
|
||||
packaging = ">=22"
|
||||
pluggy = ">=1.5,<2"
|
||||
pygments = ">=2.7.2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "7.0.0"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"},
|
||||
{file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
coverage = {version = ">=7.10.6", extras = ["toml"]}
|
||||
pluggy = ">=1.2"
|
||||
pytest = ">=7"
|
||||
|
||||
[package.extras]
|
||||
testing = ["process-tests", "pytest-xdist", "virtualenv"]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"},
|
||||
{file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
|
||||
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
|
||||
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
|
||||
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"},
|
||||
{file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"},
|
||||
{file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"},
|
||||
{file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"},
|
||||
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.3.0"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"},
|
||||
{file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"},
|
||||
{file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"},
|
||||
{file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"},
|
||||
{file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"},
|
||||
{file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"},
|
||||
{file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"},
|
||||
{file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.35.4"
|
||||
description = "Virtual Python Environment builder"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b"},
|
||||
{file = "virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
distlib = ">=0.3.7,<1"
|
||||
filelock = ">=3.12.2,<4"
|
||||
platformdirs = ">=3.9.1,<5"
|
||||
typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.13"
|
||||
content-hash = "f01b553f3895e558c34b4f10542e05acdef39bf0527c8090bd136d914dc73f94"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "98acd9fd57ec90c98a407b83122fd9c8ed432383e095a47d44e201bf187d3107"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "mirro"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
description = "A safe editing wrapper: edits a temp copy, compares, and saves original backup if changed."
|
||||
authors = ["Marco D'Aleo <marco@marcodaleo.com>"]
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -10,11 +10,19 @@ repository = "https://github.com/mdaleo404/mirro"
|
||||
packages = [{include = "mirro", from = "src"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.13"
|
||||
python = ">=3.10,<4.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
mirro = "mirro.main:main"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^9.0.1"
|
||||
pytest-cov = "^7.0.0"
|
||||
pre-commit = "^3.8"
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import importlib.metadata
|
||||
import argparse
|
||||
import tempfile
|
||||
import subprocess
|
||||
import os
|
||||
import textwrap
|
||||
import difflib
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
|
||||
def get_version():
|
||||
try:
|
||||
return importlib.metadata.version("mirro")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def read_file(path: Path) -> str:
|
||||
if not path.exists():
|
||||
return ""
|
||||
@@ -16,12 +26,14 @@ def write_file(path: Path, content: str):
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def backup_original(original_path: Path, original_content: str, backup_dir: Path) -> Path:
|
||||
def backup_original(
|
||||
original_path: Path, original_content: str, backup_dir: Path
|
||||
) -> Path:
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())
|
||||
shortstamp = time.strftime("%Y%m%dT%H%M%S", time.gmtime())
|
||||
|
||||
backup_name = f"{original_path.name}.orig.{shortstamp}.bak"
|
||||
backup_name = f"{original_path.name}.orig.{shortstamp}"
|
||||
backup_path = backup_dir / backup_name
|
||||
|
||||
header = (
|
||||
@@ -38,11 +50,36 @@ def backup_original(original_path: Path, original_content: str, backup_dir: Path
|
||||
return backup_path
|
||||
|
||||
|
||||
def strip_mirro_header(text: str) -> str:
|
||||
"""
|
||||
Strip only mirro's backup header (if present).
|
||||
Never removes shebangs or anything else.
|
||||
"""
|
||||
lines = text.splitlines(keepends=True)
|
||||
|
||||
# If there's no mirro header, return the text unchanged
|
||||
if not lines or not lines[0].startswith(
|
||||
"# ---------------------------------------------------"
|
||||
):
|
||||
return text
|
||||
|
||||
# Otherwise skip all header lines until the first blank line
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
if lines[i].strip() == "":
|
||||
i += 1 # skip the blank separator line
|
||||
break
|
||||
i += 1
|
||||
|
||||
# 'i' now points to the first real line of the original file
|
||||
return "".join(lines[i:])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Safely edit a file with automatic original backup if changed."
|
||||
)
|
||||
parser.add_argument("file", type=str, help="Path to file to edit")
|
||||
|
||||
parser.add_argument(
|
||||
"--backup-dir",
|
||||
type=str,
|
||||
@@ -50,13 +87,296 @@ def main():
|
||||
help="Backup directory",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"mirro {get_version()}",
|
||||
)
|
||||
|
||||
editor = os.environ.get("EDITOR","nano")
|
||||
target = Path(args.file).expanduser().resolve()
|
||||
backup_dir = Path(args.backup_dir).expanduser().resolve()
|
||||
parser.add_argument(
|
||||
"--list",
|
||||
action="store_true",
|
||||
help="List all backups in the backup directory and exit",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--restore-last",
|
||||
metavar="FILE",
|
||||
type=str,
|
||||
help="Restore the last backup of the given file and exit",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--prune-backups",
|
||||
nargs="?",
|
||||
const="default",
|
||||
help="Prune backups older than MIRRO_BACKUPS_LIFE days, or 'all' to delete all backups",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--diff",
|
||||
nargs=2,
|
||||
metavar=("FILE", "BACKUP"),
|
||||
help="Show a unified diff between FILE and BACKUP and exit",
|
||||
)
|
||||
|
||||
# Parse only options. Leave everything else untouched.
|
||||
args, positional = parser.parse_known_args()
|
||||
|
||||
if args.diff:
|
||||
file_arg, backup_arg = args.diff
|
||||
|
||||
file_path = Path(file_arg).expanduser().resolve()
|
||||
|
||||
# Resolve backup: if it’s not absolute or ~, treat it as a filename in the backup dir
|
||||
if os.path.isabs(backup_arg) or backup_arg.startswith("~"):
|
||||
backup_path = Path(backup_arg).expanduser().resolve()
|
||||
else:
|
||||
backup_dir = Path(args.backup_dir).expanduser().resolve()
|
||||
backup_path = backup_dir / backup_arg
|
||||
|
||||
if not file_path.exists():
|
||||
print(f"File not found: {file_path}")
|
||||
return 1
|
||||
if not backup_path.exists():
|
||||
print(f"Backup not found: {backup_path}")
|
||||
return 1
|
||||
|
||||
# Enforce same base filename while diffing
|
||||
target_name = file_path.name
|
||||
backup_name = backup_path.name
|
||||
|
||||
if not backup_name.startswith(target_name + ".orig."):
|
||||
print(
|
||||
f"Error: Backup '{backup_name}' does not match the file being diffed.\n"
|
||||
f"Expected backup file starting with: {target_name}.orig."
|
||||
)
|
||||
return 1
|
||||
|
||||
original = file_path.read_text(
|
||||
encoding="utf-8", errors="replace"
|
||||
).splitlines()
|
||||
backup_raw = backup_path.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
backup_stripped = strip_mirro_header(backup_raw)
|
||||
backup = backup_stripped.splitlines()
|
||||
|
||||
# Generate a clean diff (no trailing line noise)
|
||||
diff = difflib.unified_diff(
|
||||
backup,
|
||||
original,
|
||||
fromfile=f"a/{file_path.name}",
|
||||
tofile=f"b/{file_path.name}",
|
||||
lineterm="",
|
||||
)
|
||||
|
||||
# Colors
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
CYAN = "\033[36m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
for line in diff:
|
||||
if (
|
||||
line.startswith("---")
|
||||
or line.startswith("+++")
|
||||
or line.startswith("@@")
|
||||
):
|
||||
print(f"{CYAN}{line}{RESET}")
|
||||
elif line.startswith("+"):
|
||||
print(f"{GREEN}{line}{RESET}")
|
||||
elif line.startswith("-"):
|
||||
print(f"{RED}{line}{RESET}")
|
||||
else:
|
||||
print(line)
|
||||
|
||||
return
|
||||
|
||||
if args.list:
|
||||
import pwd, grp
|
||||
|
||||
backup_dir = Path(args.backup_dir).expanduser().resolve()
|
||||
if not backup_dir.exists():
|
||||
print("No backups found.")
|
||||
return
|
||||
|
||||
backups = sorted(
|
||||
backup_dir.iterdir(), key=os.path.getmtime, reverse=True
|
||||
)
|
||||
if not backups:
|
||||
print("No backups found.")
|
||||
return
|
||||
|
||||
def perms(mode):
|
||||
is_file = "-"
|
||||
perms = ""
|
||||
flags = [
|
||||
(mode & 0o400, "r"),
|
||||
(mode & 0o200, "w"),
|
||||
(mode & 0o100, "x"),
|
||||
(mode & 0o040, "r"),
|
||||
(mode & 0o020, "w"),
|
||||
(mode & 0o010, "x"),
|
||||
(mode & 0o004, "r"),
|
||||
(mode & 0o002, "w"),
|
||||
(mode & 0o001, "x"),
|
||||
]
|
||||
for bit, char in flags:
|
||||
perms += char if bit else "-"
|
||||
return is_file + perms
|
||||
|
||||
for b in backups:
|
||||
stat = b.stat()
|
||||
mode = perms(stat.st_mode)
|
||||
|
||||
try:
|
||||
owner = pwd.getpwuid(stat.st_uid).pw_name
|
||||
except KeyError:
|
||||
owner = str(stat.st_uid)
|
||||
|
||||
try:
|
||||
group = grp.getgrgid(stat.st_gid).gr_name
|
||||
except KeyError:
|
||||
group = str(stat.st_gid)
|
||||
|
||||
owner_group = f"{owner} {group}"
|
||||
|
||||
mtime = time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S", time.gmtime(stat.st_mtime)
|
||||
)
|
||||
|
||||
print(f"{mode:11} {owner_group:20} {mtime} {b.name}")
|
||||
|
||||
return
|
||||
|
||||
if args.restore_last:
|
||||
backup_dir = Path(args.backup_dir).expanduser().resolve()
|
||||
target = Path(args.restore_last).expanduser().resolve()
|
||||
|
||||
if not backup_dir.exists():
|
||||
print("No backup directory found.")
|
||||
return 1
|
||||
|
||||
# backup filenames look like: <name>.orig.<timestamp>
|
||||
prefix = f"{target.name}.orig."
|
||||
|
||||
backups = [
|
||||
b for b in backup_dir.iterdir() if b.name.startswith(prefix)
|
||||
]
|
||||
|
||||
if not backups:
|
||||
print(f"No backups found for {target}")
|
||||
return 1
|
||||
|
||||
# newest backup
|
||||
last = max(backups, key=os.path.getmtime)
|
||||
|
||||
# read and strip header
|
||||
raw = last.read_text(encoding="utf-8", errors="replace")
|
||||
restored_text = strip_mirro_header(raw)
|
||||
target.write_text(restored_text, encoding="utf-8")
|
||||
|
||||
print(f"Restored {target} from backup {last.name}")
|
||||
return
|
||||
|
||||
if args.prune_backups is not None:
|
||||
mode = args.prune_backups
|
||||
|
||||
# ALL mode
|
||||
if mode == "all":
|
||||
prune_days = None
|
||||
|
||||
# default
|
||||
elif mode == "default":
|
||||
raw_env = os.environ.get("MIRRO_BACKUPS_LIFE", "30")
|
||||
try:
|
||||
prune_days = int(raw_env)
|
||||
if prune_days < 1:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
print(
|
||||
f"Invalid MIRRO_BACKUPS_LIFE value: {raw_env}. "
|
||||
"It must be an integer >= 1. Falling back to 30."
|
||||
)
|
||||
prune_days = 30
|
||||
|
||||
# numeric mode e.g. --prune-backups=7
|
||||
else:
|
||||
try:
|
||||
prune_days = int(mode)
|
||||
if prune_days < 1:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
msg = f"""
|
||||
Invalid value for --prune-backups: {mode}
|
||||
|
||||
--prune-backups use MIRRO_BACKUPS_LIFE (default: 30 days)
|
||||
--prune-backups=N expire backups older than N days (N >= 1)
|
||||
--prune-backups=all remove ALL backups
|
||||
"""
|
||||
print(textwrap.dedent(msg))
|
||||
return 1
|
||||
|
||||
backup_dir = Path(args.backup_dir).expanduser().resolve()
|
||||
|
||||
if not backup_dir.exists():
|
||||
print("No backup directory found.")
|
||||
return 0
|
||||
|
||||
# prune EVERYTHING
|
||||
if prune_days is None:
|
||||
removed = []
|
||||
for b in backup_dir.iterdir():
|
||||
if b.is_file():
|
||||
removed.append(b)
|
||||
b.unlink()
|
||||
print(f"Removed ALL backups ({len(removed)} file(s)).")
|
||||
return 0
|
||||
|
||||
# prune by age
|
||||
cutoff = time.time() - (prune_days * 86400)
|
||||
removed = []
|
||||
|
||||
for b in backup_dir.iterdir():
|
||||
if b.is_file() and b.stat().st_mtime < cutoff:
|
||||
removed.append(b)
|
||||
b.unlink()
|
||||
|
||||
if removed:
|
||||
print(
|
||||
f"Removed {len(removed)} backup(s) older than {prune_days} days."
|
||||
)
|
||||
else:
|
||||
print(f"No backups older than {prune_days} days.")
|
||||
|
||||
return 0
|
||||
|
||||
# Flexible positional parsing
|
||||
if not positional:
|
||||
parser.error("the following arguments are required: file")
|
||||
|
||||
file_arg = None
|
||||
editor_extra = []
|
||||
|
||||
for p in positional:
|
||||
if (
|
||||
file_arg is None
|
||||
and not p.startswith("+")
|
||||
and not p.startswith("-")
|
||||
):
|
||||
file_arg = p
|
||||
else:
|
||||
editor_extra.append(p)
|
||||
|
||||
if file_arg is None:
|
||||
parser.error("the following arguments are required: file")
|
||||
|
||||
editor = os.environ.get("EDITOR", "nano")
|
||||
editor_cmd = editor.split()
|
||||
|
||||
target = Path(file_arg).expanduser().resolve()
|
||||
backup_dir = Path(args.backup_dir).expanduser().resolve()
|
||||
|
||||
# Permission checks
|
||||
parent = target.parent
|
||||
if target.exists() and not os.access(target, os.W_OK):
|
||||
@@ -77,11 +397,13 @@ def main():
|
||||
delete=False, prefix="mirro-", suffix=target.suffix
|
||||
) as tf:
|
||||
temp_path = Path(tf.name)
|
||||
# Write prepopulated or original content to temp file
|
||||
|
||||
write_file(temp_path, original_content)
|
||||
|
||||
# Launch editor
|
||||
subprocess.call(editor_cmd + [str(temp_path)])
|
||||
if "nano" in editor_cmd[0]:
|
||||
subprocess.call(editor_cmd + editor_extra + [str(temp_path)])
|
||||
else:
|
||||
subprocess.call(editor_cmd + [str(temp_path)] + editor_extra)
|
||||
|
||||
# Read edited
|
||||
edited_content = read_file(temp_path)
|
||||
|
||||
567
tests/test_mirro.py
Normal file
567
tests/test_mirro.py
Normal file
@@ -0,0 +1,567 @@
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import mirro.main as mirro
|
||||
|
||||
|
||||
# ============================================================
|
||||
# get_version
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_get_version_found(monkeypatch):
|
||||
monkeypatch.setattr(mirro.importlib.metadata, "version", lambda _: "1.2.3")
|
||||
assert mirro.get_version() == "1.2.3"
|
||||
|
||||
|
||||
def test_get_version_not_found(monkeypatch):
|
||||
def raiser(_):
|
||||
raise mirro.importlib.metadata.PackageNotFoundError
|
||||
|
||||
monkeypatch.setattr(mirro.importlib.metadata, "version", raiser)
|
||||
assert mirro.get_version() == "unknown"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# read_file / write_file
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_read_file_exists(tmp_path):
|
||||
p = tmp_path / "x.txt"
|
||||
p.write_text("hello\n", encoding="utf-8")
|
||||
assert mirro.read_file(p) == "hello\n"
|
||||
|
||||
|
||||
def test_read_file_missing(tmp_path):
|
||||
assert mirro.read_file(tmp_path / "nope.txt") == ""
|
||||
|
||||
|
||||
def test_write_file(tmp_path):
|
||||
p = tmp_path / "y.txt"
|
||||
mirro.write_file(p, "data")
|
||||
assert p.read_text(encoding="utf-8") == "data"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# strip_mirro_header
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_strip_header_removes_header():
|
||||
header_text = (
|
||||
"# ---------------------------------------------------\n"
|
||||
"# mirro backup\n"
|
||||
"# something\n"
|
||||
"# ---------------------------------------------------\n"
|
||||
"\n"
|
||||
"#!/bin/bash\n"
|
||||
"echo hi\n"
|
||||
)
|
||||
|
||||
out = mirro.strip_mirro_header(header_text)
|
||||
assert out.startswith("#!/bin/bash")
|
||||
assert "mirro backup" not in out
|
||||
|
||||
|
||||
def test_strip_header_preserves_shebang():
|
||||
text = "#!/usr/bin/env python3\nprint('hi')\n"
|
||||
out = mirro.strip_mirro_header(text)
|
||||
assert out == text # unchanged
|
||||
|
||||
|
||||
def test_strip_header_non_header_file():
|
||||
text = "# just a comment\nvalue\n"
|
||||
out = mirro.strip_mirro_header(text)
|
||||
assert out == text
|
||||
|
||||
|
||||
# ============================================================
|
||||
# backup_original
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_backup_original(tmp_path, monkeypatch):
|
||||
original_path = tmp_path / "a.txt"
|
||||
original_content = "ABC"
|
||||
backup_dir = tmp_path / "backups"
|
||||
|
||||
monkeypatch.setattr(
|
||||
time,
|
||||
"gmtime",
|
||||
lambda: time.struct_time((2023, 1, 2, 3, 4, 5, 0, 0, 0)),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
time,
|
||||
"strftime",
|
||||
lambda fmt, _: {
|
||||
"%Y-%m-%d %H:%M:%S UTC": "2023-01-02 03:04:05 UTC",
|
||||
"%Y%m%dT%H%M%S": "20230102T030405",
|
||||
}[fmt],
|
||||
)
|
||||
|
||||
backup_path = mirro.backup_original(
|
||||
original_path, original_content, backup_dir
|
||||
)
|
||||
|
||||
assert backup_path.exists()
|
||||
text = backup_path.read_text()
|
||||
assert "mirro backup" in text
|
||||
assert "Original file" in text
|
||||
assert "ABC" in text
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helper to simulate main()
|
||||
# ============================================================
|
||||
|
||||
|
||||
def simulate_main(
|
||||
monkeypatch,
|
||||
capsys,
|
||||
args,
|
||||
*,
|
||||
editor="nano",
|
||||
start_content=None,
|
||||
edited_content=None,
|
||||
file_exists=True,
|
||||
override_access=None,
|
||||
):
|
||||
monkeypatch.setenv("EDITOR", editor)
|
||||
|
||||
def fake_call(cmd):
|
||||
temp = Path(cmd[-1])
|
||||
if edited_content is None:
|
||||
temp.write_text(start_content or "", encoding="utf-8")
|
||||
else:
|
||||
temp.write_text(edited_content, encoding="utf-8")
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(subprocess, "call", fake_call)
|
||||
|
||||
if override_access:
|
||||
monkeypatch.setattr(os, "access", override_access)
|
||||
else:
|
||||
monkeypatch.setattr(os, "access", lambda p, m: True)
|
||||
|
||||
target = Path(args[-1]).expanduser().resolve()
|
||||
if file_exists:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(start_content or "", encoding="utf-8")
|
||||
|
||||
with patch("sys.argv", ["mirro"] + args):
|
||||
result = mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
return result, out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# main: missing positional file
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_main_missing_argument(capsys):
|
||||
with patch("sys.argv", ["mirro"]):
|
||||
with pytest.raises(SystemExit):
|
||||
mirro.main()
|
||||
assert (
|
||||
"the following arguments are required: file" in capsys.readouterr().err
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# main: unchanged file
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_main_existing_unchanged(tmp_path, monkeypatch, capsys):
|
||||
target = tmp_path / "file.txt"
|
||||
target.write_text("hello\n")
|
||||
|
||||
def fake_call(cmd):
|
||||
temp = Path(cmd[-1])
|
||||
temp.write_text("hello\n")
|
||||
|
||||
monkeypatch.setenv("EDITOR", "nano")
|
||||
monkeypatch.setattr(subprocess, "call", fake_call)
|
||||
monkeypatch.setattr(os, "access", lambda p, m: True)
|
||||
|
||||
with patch("sys.argv", ["mirro", str(target)]):
|
||||
mirro.main()
|
||||
|
||||
assert "file hasn't changed" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# main: changed file
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_main_existing_changed(tmp_path, monkeypatch, capsys):
|
||||
target = tmp_path / "f2.txt"
|
||||
|
||||
result, out = simulate_main(
|
||||
monkeypatch,
|
||||
capsys,
|
||||
args=[str(target)],
|
||||
start_content="old\n",
|
||||
edited_content="new\n",
|
||||
file_exists=True,
|
||||
)
|
||||
|
||||
assert "file changed; original backed up at" in out
|
||||
assert target.read_text() == "new\n"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# main: new file unchanged
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_main_new_file_unchanged(tmp_path, monkeypatch, capsys):
|
||||
new = tmp_path / "new.txt"
|
||||
|
||||
result, out = simulate_main(
|
||||
monkeypatch,
|
||||
capsys,
|
||||
args=[str(new)],
|
||||
start_content=None,
|
||||
edited_content="This is a new file created with 'mirro'!\n",
|
||||
file_exists=False,
|
||||
)
|
||||
|
||||
assert "file hasn't changed" in out
|
||||
assert not new.exists()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# main: new file changed
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_main_new_file_changed(tmp_path, monkeypatch, capsys):
|
||||
new = tmp_path / "new2.txt"
|
||||
|
||||
result, out = simulate_main(
|
||||
monkeypatch,
|
||||
capsys,
|
||||
args=[str(new)],
|
||||
start_content=None,
|
||||
edited_content="XYZ\n",
|
||||
file_exists=False,
|
||||
)
|
||||
|
||||
assert "file changed; original backed up at" in out
|
||||
assert new.read_text() == "XYZ\n"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Permission denied branches
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_main_permission_denied_existing(tmp_path, monkeypatch, capsys):
|
||||
tgt = tmp_path / "blocked.txt"
|
||||
tgt.write_text("hi")
|
||||
|
||||
monkeypatch.setenv("EDITOR", "nano")
|
||||
monkeypatch.setattr(os, "access", lambda p, m: False)
|
||||
|
||||
with patch("sys.argv", ["mirro", str(tgt)]):
|
||||
result = mirro.main()
|
||||
|
||||
assert result == 1
|
||||
assert "Need elevated privileges to open" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_main_permission_denied_create(tmp_path, monkeypatch, capsys):
|
||||
new = tmp_path / "sub/xx.txt"
|
||||
new.parent.mkdir(parents=True)
|
||||
|
||||
def fake_access(path, mode):
|
||||
return False if path == new.parent else True
|
||||
|
||||
monkeypatch.setattr(os, "access", fake_access)
|
||||
monkeypatch.setenv("EDITOR", "nano")
|
||||
|
||||
with patch("sys.argv", ["mirro", str(new)]):
|
||||
result = mirro.main()
|
||||
|
||||
assert result == 1
|
||||
assert "Need elevated privileges to create" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Editor ordering: non-nano branch
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_main_editor_non_nano(tmp_path, monkeypatch, capsys):
|
||||
target = tmp_path / "vim.txt"
|
||||
target.write_text("old\n")
|
||||
|
||||
def fake_call(cmd):
|
||||
temp = Path(cmd[1])
|
||||
temp.write_text("edited\n")
|
||||
|
||||
monkeypatch.setenv("EDITOR", "vim")
|
||||
monkeypatch.setattr(subprocess, "call", fake_call)
|
||||
monkeypatch.setattr(os, "access", lambda p, m: True)
|
||||
|
||||
with patch("sys.argv", ["mirro", str(target)]):
|
||||
mirro.main()
|
||||
|
||||
assert target.read_text() == "edited\n"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# --list
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_main_list_no_dir(tmp_path, capsys):
|
||||
with patch(
|
||||
"sys.argv", ["mirro", "--list", "--backup-dir", str(tmp_path / "none")]
|
||||
):
|
||||
mirro.main()
|
||||
assert "No backups found." in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_main_list_entries(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
(d / "a.txt.orig.1").write_text("x")
|
||||
(d / "b.txt.orig.2").write_text("y")
|
||||
|
||||
with patch("sys.argv", ["mirro", "--list", "--backup-dir", str(d)]):
|
||||
mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "a.txt.orig.1" in out
|
||||
assert "b.txt.orig.2" in out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# --restore-last
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_restore_last_no_dir(tmp_path, capsys):
|
||||
d = tmp_path / "none"
|
||||
target = tmp_path / "x.txt"
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["mirro", "--restore-last", str(target), "--backup-dir", str(d)],
|
||||
):
|
||||
result = mirro.main()
|
||||
|
||||
assert result == 1
|
||||
assert "No backup directory found." in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_restore_last_no_backups(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
target = tmp_path / "t.txt"
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["mirro", "--restore-last", str(target), "--backup-dir", str(d)],
|
||||
):
|
||||
result = mirro.main()
|
||||
|
||||
assert result == 1
|
||||
assert "No backups found" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_restore_last_success(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
target = tmp_path / "t.txt"
|
||||
|
||||
mirro_header = (
|
||||
"# ---------------------------------------------------\n"
|
||||
"# mirro backup\n"
|
||||
"# Original file: x\n"
|
||||
"# Timestamp: test\n"
|
||||
"# Delete this header if you want to restore the file\n"
|
||||
"# ---------------------------------------------------\n"
|
||||
"\n"
|
||||
)
|
||||
|
||||
b1 = d / "t.txt.orig.2020"
|
||||
b2 = d / "t.txt.orig.2021"
|
||||
|
||||
b1.write_text(mirro_header + "old1")
|
||||
b2.write_text(mirro_header + "old2")
|
||||
|
||||
# ensure newest
|
||||
os.utime(b2, (time.time(), time.time()))
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["mirro", "--restore-last", str(target), "--backup-dir", str(d)],
|
||||
):
|
||||
mirro.main()
|
||||
|
||||
assert target.read_text() == "old2"
|
||||
assert "Restored" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# --prune-backups
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_prune_all(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
(d / "a").write_text("x")
|
||||
(d / "b").write_text("y")
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["mirro", "--prune-backups=all", "--backup-dir", str(d)]
|
||||
):
|
||||
mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Removed ALL backups" in out
|
||||
assert not any(d.iterdir())
|
||||
|
||||
|
||||
def test_prune_numeric(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
|
||||
old = d / "old"
|
||||
new = d / "new"
|
||||
old.write_text("x")
|
||||
new.write_text("y")
|
||||
|
||||
one_day_seconds = 86400
|
||||
|
||||
os.utime(
|
||||
old,
|
||||
(
|
||||
time.time() - one_day_seconds * 10,
|
||||
time.time() - one_day_seconds * 10,
|
||||
),
|
||||
)
|
||||
os.utime(new, None)
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["mirro", "--prune-backups=5", "--backup-dir", str(d)]
|
||||
):
|
||||
mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Removed 1" in out
|
||||
assert new.exists()
|
||||
assert not old.exists()
|
||||
|
||||
|
||||
def test_prune_default_env(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setenv("MIRRO_BACKUPS_LIFE", "1")
|
||||
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
|
||||
f = d / "x"
|
||||
f.write_text("hi")
|
||||
|
||||
os.utime(f, (time.time() - 86400 * 2, time.time() - 86400 * 2))
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["mirro", "--prune-backups", "--backup-dir", str(d)]
|
||||
):
|
||||
mirro.main()
|
||||
|
||||
assert "Removed 1" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_prune_invalid_env(tmp_path, monkeypatch, capsys):
|
||||
monkeypatch.setenv("MIRRO_BACKUPS_LIFE", "nope")
|
||||
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["mirro", "--prune-backups", "--backup-dir", str(d)]
|
||||
):
|
||||
mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Invalid MIRRO_BACKUPS_LIFE value" in out
|
||||
|
||||
|
||||
def test_prune_invalid_arg(tmp_path, capsys):
|
||||
with patch("sys.argv", ["mirro", "--prune-backups=zzz"]):
|
||||
result = mirro.main()
|
||||
|
||||
assert result == 1
|
||||
assert "Invalid value for --prune-backups" in capsys.readouterr().out
|
||||
|
||||
|
||||
# ============================================================
|
||||
# --diff tests
|
||||
# ============================================================
|
||||
|
||||
|
||||
def test_diff_basic(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
|
||||
file = tmp_path / "t.txt"
|
||||
file.write_text("line1\nline2\n")
|
||||
|
||||
backup = d / "t.txt.orig.20250101T010203"
|
||||
backup.write_text(
|
||||
"# ---------------------------------------------------\n"
|
||||
"# mirro backup\n"
|
||||
"# whatever\n"
|
||||
"\n"
|
||||
"line1\nold\n"
|
||||
)
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["mirro", "--diff", str(file), backup.name, "--backup-dir", str(d)],
|
||||
):
|
||||
mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
|
||||
assert "--- a/t.txt" in out
|
||||
assert "+++ b/t.txt" in out
|
||||
assert "@@" in out
|
||||
assert "-old" in out
|
||||
assert "+line2" in out
|
||||
|
||||
|
||||
def test_diff_wrong_backup_name_rejected(tmp_path, capsys):
|
||||
d = tmp_path / "bk"
|
||||
d.mkdir()
|
||||
|
||||
file = tmp_path / "foo.txt"
|
||||
file.write_text("hello\n")
|
||||
|
||||
bad = d / "bar.txt.orig.20250101T010203"
|
||||
bad.write_text("stuff\n")
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["mirro", "--diff", str(file), bad.name, "--backup-dir", str(d)],
|
||||
):
|
||||
result = mirro.main()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
|
||||
assert result == 1
|
||||
assert "does not match the file being diffed" in out
|
||||
assert "foo.txt.orig." in out
|
||||
Reference in New Issue
Block a user