Fix restore preview to document skipped missing and type drift
This commit is contained in:
@@ -19,10 +19,13 @@ applies changes after explicit confirmation.
|
||||
## Features
|
||||
|
||||
### Snapshots ownership and permissions
|
||||
Records numeric `uid`, `gid`, and file mode for files and directories.
|
||||
Records numeric `uid`, `gid`, and file mode for files, directories, and
|
||||
symbolic links.
|
||||
|
||||
### Preview before restore
|
||||
Always shows a clear, readable table of differences before applying changes.
|
||||
Missing paths and type mismatches are shown as skipped items because restore does
|
||||
not create, delete, or replace files.
|
||||
|
||||
### Interactive confirmation
|
||||
A single confirmation prompt at the end of a restore (default: **No**).
|
||||
@@ -65,7 +68,7 @@ Restore:
|
||||
* Never creates, deletes, or moves files
|
||||
* Missing files are ignored
|
||||
* New files are ignored
|
||||
* Symbolic links are skipped entirely
|
||||
* Symbolic links are handled without following their targets
|
||||
* Requires sudo **only when necessary**
|
||||
|
||||
## Non-Goals
|
||||
@@ -178,7 +181,8 @@ app-baseline /srv/app 2025-12-20 18:11:08 +00:00
|
||||
chguard --restore app-baseline
|
||||
```
|
||||
|
||||
This shows a table of ownership and permission differences.
|
||||
This shows a table of ownership and permission differences. Missing paths and
|
||||
type mismatches are reported as skipped items.
|
||||
|
||||
### Restore with confirmation
|
||||
```
|
||||
@@ -246,7 +250,7 @@ chguard -- chgrp staff file
|
||||
Snapshots are stored in a local SQLite database containing:
|
||||
|
||||
* relative path
|
||||
* file type (file or directory)
|
||||
* file type (file, directory, or symbolic link)
|
||||
* numeric uid / gid
|
||||
* numeric mode
|
||||
|
||||
@@ -271,3 +275,11 @@ poetry install
|
||||
poetry run pre-commit install
|
||||
```
|
||||
This ensures consistent formatting, catches common issues early, and keeps the codebase clean.
|
||||
|
||||
## Tests
|
||||
|
||||
Run the pytest suite with:
|
||||
|
||||
```
|
||||
poetry run pytest
|
||||
```
|
||||
|
||||
+92
-45
@@ -27,7 +27,7 @@ from chguard.db import (
|
||||
prune_states_before,
|
||||
state_exists,
|
||||
)
|
||||
from chguard.restore import apply_restore, plan_restore
|
||||
from chguard.restore import PlannedChange, apply_restore, plan_restore
|
||||
from chguard.scan import scan_tree
|
||||
from chguard.util import normalize_root
|
||||
|
||||
@@ -270,6 +270,74 @@ def _captured_paths_summary(
|
||||
return summary
|
||||
|
||||
|
||||
def _display_restore_path(path: Path, target_root: Path) -> Path:
|
||||
try:
|
||||
return path.relative_to(target_root)
|
||||
except ValueError:
|
||||
return path
|
||||
|
||||
|
||||
def _format_skipped_restore_change(change: PlannedChange) -> str:
|
||||
if change.kind == "missing":
|
||||
return "missing path"
|
||||
|
||||
if change.kind == "type":
|
||||
got, expected = change.detail.split(" -> ", 1)
|
||||
return f"found {got}, expected {expected}"
|
||||
|
||||
return change.detail
|
||||
|
||||
|
||||
def _restore_preview_rows(
|
||||
changes: list[PlannedChange], target_root: Path, current_uid: int
|
||||
) -> tuple[dict[Path, dict[str, str]], Counter, bool]:
|
||||
per_path: dict[Path, dict[str, str]] = defaultdict(dict)
|
||||
counts = Counter()
|
||||
needs_root = False
|
||||
|
||||
for ch in changes:
|
||||
rel = _display_restore_path(ch.path, target_root)
|
||||
|
||||
if ch.kind == "owner" and ch.will_apply:
|
||||
before, after = ch.detail.split(" -> ")
|
||||
bu, bg = map(int, before.split(":"))
|
||||
au, ag = map(int, after.split(":"))
|
||||
|
||||
owner_change = f"{_format_owner(bu, bg)} → {_format_owner(au, ag)}"
|
||||
per_path[rel]["owner"] = owner_change
|
||||
counts["owner"] += 1
|
||||
|
||||
try:
|
||||
if ch.path.lstat().st_uid != current_uid:
|
||||
needs_root = True
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
elif ch.kind == "mode" and ch.will_apply:
|
||||
before, after = ch.detail.split(" -> ")
|
||||
per_path[rel]["mode"] = (
|
||||
f"{_mode_to_rwx(int(before, 8))} → "
|
||||
f"{_mode_to_rwx(int(after, 8))}"
|
||||
)
|
||||
counts["mode"] += 1
|
||||
|
||||
try:
|
||||
if ch.path.lstat().st_uid != current_uid:
|
||||
needs_root = True
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
elif ch.kind in ("missing", "type"):
|
||||
skipped = _format_skipped_restore_change(ch)
|
||||
existing = per_path[rel].get("skipped")
|
||||
per_path[rel]["skipped"] = (
|
||||
f"{existing}; {skipped}" if existing else skipped
|
||||
)
|
||||
counts["skipped"] += 1
|
||||
|
||||
return per_path, counts, needs_root
|
||||
|
||||
|
||||
def main() -> None:
|
||||
wrapper_cmd = None
|
||||
if "--" in sys.argv:
|
||||
@@ -607,51 +675,18 @@ def main() -> None:
|
||||
restore_owner=restore_owner,
|
||||
)
|
||||
|
||||
per_path: dict[Path, dict[str, str]] = defaultdict(dict)
|
||||
counts = Counter()
|
||||
needs_root = False
|
||||
current_uid = os.geteuid()
|
||||
per_path, counts, needs_root = _restore_preview_rows(
|
||||
changes, target_root, os.geteuid()
|
||||
)
|
||||
|
||||
for ch in changes:
|
||||
if ch.kind not in ("owner", "mode"):
|
||||
continue
|
||||
|
||||
try:
|
||||
rel = ch.path.relative_to(target_root)
|
||||
except ValueError:
|
||||
rel = ch.path
|
||||
|
||||
if ch.kind == "owner" and restore_owner:
|
||||
before, after = ch.detail.split(" -> ")
|
||||
bu, bg = map(int, before.split(":"))
|
||||
au, ag = map(int, after.split(":"))
|
||||
|
||||
per_path[rel][
|
||||
"owner"
|
||||
] = f"{_format_owner(bu, bg)} → {_format_owner(au, ag)}"
|
||||
counts["owner"] += 1
|
||||
|
||||
try:
|
||||
if ch.path.stat().st_uid != current_uid:
|
||||
needs_root = True
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
elif ch.kind == "mode" and restore_permissions:
|
||||
b, a = ch.detail.split(" -> ")
|
||||
per_path[rel][
|
||||
"mode"
|
||||
] = f"{_mode_to_rwx(int(b, 8))} → {_mode_to_rwx(int(a, 8))}"
|
||||
counts["mode"] += 1
|
||||
|
||||
try:
|
||||
if ch.path.stat().st_uid != current_uid:
|
||||
needs_root = True
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if not changes:
|
||||
console.print("No differences found.")
|
||||
return
|
||||
|
||||
if not per_path:
|
||||
console.print("No differences found.")
|
||||
console.print(
|
||||
"No differences found for the selected restore scope."
|
||||
)
|
||||
return
|
||||
|
||||
console.print(f"\nRestoring under: {target_root}\n")
|
||||
@@ -660,19 +695,31 @@ def main() -> None:
|
||||
table.add_column("Path")
|
||||
table.add_column("Owner change", style="cyan")
|
||||
table.add_column("Mode change", style="green")
|
||||
table.add_column("Skipped", style="yellow")
|
||||
|
||||
for path in sorted(per_path):
|
||||
row = per_path[path]
|
||||
table.add_row(
|
||||
str(path), row.get("owner", "—"), row.get("mode", "—")
|
||||
str(path),
|
||||
row.get("owner", "—"),
|
||||
row.get("mode", "—"),
|
||||
row.get("skipped", "—"),
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
console.print(
|
||||
f"\nSummary: {counts['mode']} mode change(s), "
|
||||
f"{counts['owner']} owner change(s)"
|
||||
f"{counts['owner']} owner change(s), "
|
||||
f"{counts['skipped']} skipped item(s)"
|
||||
)
|
||||
|
||||
if counts["mode"] == 0 and counts["owner"] == 0:
|
||||
console.print(
|
||||
"\n[yellow]No applicable changes. "
|
||||
"Skipped items were not restored.[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
if args.dry_run:
|
||||
console.print(
|
||||
"\n[yellow]Dry-run only. No changes were applied.[/yellow]"
|
||||
|
||||
Generated
+157
-3
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "argcomplete"
|
||||
@@ -27,6 +27,19 @@ files = [
|
||||
{file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"},
|
||||
]
|
||||
|
||||
[[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"
|
||||
groups = ["dev"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
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 = "distlib"
|
||||
version = "0.4.0"
|
||||
@@ -39,6 +52,25 @@ files = [
|
||||
{file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.1"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
markers = "python_version == \"3.10\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"},
|
||||
{file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""}
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.20.3"
|
||||
@@ -66,6 +98,18 @@ files = [
|
||||
[package.extras]
|
||||
license = ["ukkonen"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
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 = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
@@ -114,6 +158,18 @@ files = [
|
||||
{file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
|
||||
{file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.5.1"
|
||||
@@ -131,6 +187,22 @@ docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-
|
||||
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"
|
||||
groups = ["dev"]
|
||||
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"
|
||||
@@ -156,7 +228,7 @@ version = "2.20.0"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
|
||||
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
|
||||
@@ -165,6 +237,30 @@ files = [
|
||||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.1"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"},
|
||||
{file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"},
|
||||
]
|
||||
|
||||
[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 = "pyyaml"
|
||||
version = "6.0.3"
|
||||
@@ -267,6 +363,64 @@ pygments = ">=2.13.0,<3.0.0"
|
||||
[package.extras]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.4.1"
|
||||
description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version == \"3.10\""
|
||||
files = [
|
||||
{file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"},
|
||||
{file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"},
|
||||
{file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"},
|
||||
{file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"},
|
||||
{file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"},
|
||||
{file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"},
|
||||
{file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"},
|
||||
{file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
@@ -305,4 +459,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "c1c99dd4ff6557dd08b58f3a8c50b893e68acabcb8b08a48ca78f7319a361635"
|
||||
content-hash = "e2c2d57a74d31e7a59cc63daae9511890c9e21db043df7a5c8d9ac33e967e393"
|
||||
|
||||
@@ -20,10 +20,14 @@ chguard = "chguard.cli:main"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pre-commit = "^3.8"
|
||||
pytest = "^9.1.1"
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
Reference in New Issue
Block a user