Compare commits

...

5 commits

Author SHA1 Message Date
952687e15d
Ensure that --include-path records (but does not traverse) symlinks
Some checks failed
Lint / test (push) Waiting to run
CI / test (push) Failing after 44s
CI / test (almalinux, docker.io/library/almalinux:9, python3.11) (push) Has been cancelled
CI / test (debian, docker.io/library/debian:13, python3) (push) Has been cancelled
2026-06-22 15:34:44 +10:00
07b07e60c5
Ensure paths are not followed through parent links 2026-06-22 15:32:40 +10:00
e10a3f62b0
Belts and braces: normalise paths before globbing 2026-06-22 15:06:46 +10:00
c4448226c0
Ensure tests run through the poetry env's pytest 2026-06-22 15:05:48 +10:00
00f960d01e
Upgrade to Poetry 2 2026-06-22 15:03:32 +10:00
16 changed files with 698 additions and 182 deletions

View file

@ -71,6 +71,7 @@ jobs:
- name: Install Poetry - name: Install Poetry
env: env:
PYTHON_BIN: ${{ matrix.python }} PYTHON_BIN: ${{ matrix.python }}
POETRY_VERSION: "2.4.1"
run: | run: |
set -eux set -eux
if ! command -v pipx >/dev/null 2>&1; then if ! command -v pipx >/dev/null 2>&1; then
@ -80,9 +81,11 @@ jobs:
if [ -z "${PIPX_BIN}" ]; then if [ -z "${PIPX_BIN}" ]; then
PIPX_BIN="${HOME}/.local/bin/pipx" PIPX_BIN="${HOME}/.local/bin/pipx"
fi fi
"${PIPX_BIN}" install --python "${PYTHON_BIN}" poetry==1.8.3 "${PIPX_BIN}" install --python "${PYTHON_BIN}" "poetry==${POETRY_VERSION}"
/root/.local/bin/poetry --version
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.local/bin:$PATH"
poetry --version
poetry --version | grep -E "Poetry \(version 2\."
- name: Install project deps (including test extras) - name: Install project deps (including test extras)
env: env:

View file

@ -146,7 +146,12 @@ def is_human_user(uid: int, shell: str, uid_min: int) -> bool:
def find_user_ssh_files(home: str) -> List[str]: def find_user_ssh_files(home: str) -> List[str]:
sshdir = os.path.join(home, ".ssh") sshdir = os.path.join(home, ".ssh")
out: List[str] = [] out: List[str] = []
if not os.path.isdir(sshdir): # ``os.path.isdir`` follows symlinks, so a user who replaces ``~/.ssh``
# with a link to a sensitive directory (e.g. /etc/ssl/private) could
# otherwise have a regular file inside it harvested through the symlinked
# parent. Refuse a symlinked .ssh outright; capture_file() applies the
# same parent-symlink protection at copy time as defense in depth.
if os.path.islink(sshdir) or not os.path.isdir(sshdir):
return out return out
ak = os.path.join(sshdir, "authorized_keys") ak = os.path.join(sshdir, "authorized_keys")

View file

@ -5,7 +5,7 @@ import errno
import stat import stat
from typing import List, Optional, Set from typing import List, Optional, Set
from .fsutil import stat_triplet, stat_triplet_from_stat from .fsutil import open_no_follow_path, stat_triplet, stat_triplet_from_stat
from .harvest_types import ExcludedFile, ManagedFile, ManagedLink from .harvest_types import ExcludedFile, ManagedFile, ManagedLink
from .ignore import IgnorePolicy from .ignore import IgnorePolicy
from .pathfilter import PathFilter from .pathfilter import PathFilter
@ -55,10 +55,7 @@ def files_differ(a: str, b: str, *, max_bytes: int = 2_000_000) -> bool:
def _open_no_follow_write(path: str, mode: int = 0o600) -> int: def _open_no_follow_write(path: str, mode: int = 0o600) -> int:
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0) return open_no_follow_path(path, write=True, mode=mode)
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
return os.open(path, flags, mode)
def write_bytes_into_bundle( def write_bytes_into_bundle(
@ -92,14 +89,10 @@ def copy_into_bundle(
symlinks at copy time and refuses destination symlink overwrites. symlinks at copy time and refuses destination symlink overwrites.
""" """
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
fd = -1 fd = -1
try: try:
try: try:
fd = os.open(abs_path, flags) fd = open_no_follow_path(abs_path)
except OSError as e: except OSError as e:
if e.errno in {errno.ELOOP, errno.ENOTDIR}: if e.errno in {errno.ELOOP, errno.ENOTDIR}:
raise OSError("refusing to copy symlink source") from e raise OSError("refusing to copy symlink source") from e

View file

@ -189,6 +189,12 @@ _EXCLUDED_REASONS: Dict[str, ReasonInfo] = {
"Not a regular file", "Not a regular file",
"Excluded because it was not a regular file (device, socket, etc.).", "Excluded because it was not a regular file (device, socket, etc.).",
), ),
"symlink_component": ReasonInfo(
"Unsafe symlinked path",
"Excluded because a directory in the path was a symlink, which could "
"redirect capture into a sensitive location; Enroll refuses to follow "
"symlinked parents when harvesting files.",
),
"binary_like": ReasonInfo( "binary_like": ReasonInfo(
"Binary-like", "Binary-like",
"Excluded because it looked like binary content (not useful for config management).", "Excluded because it looked like binary content (not useful for config management).",

View file

@ -1,10 +1,131 @@
from __future__ import annotations from __future__ import annotations
import errno
import hashlib import hashlib
import os import os
import stat
from typing import Tuple from typing import Tuple
def open_no_follow_path(path: str, *, write: bool = False, mode: int = 0o600) -> int:
"""Open ``path`` without following a symlink in *any* path component.
``O_NOFOLLOW`` only protects the final component of a path. A regular
file reached through a symlinked *parent* directory (for example a user
replacing ``~/.ssh`` with a link to a sensitive directory) would still be
opened by a plain ``os.open(path, O_NOFOLLOW)``.
This helper resolves the path one component at a time with ``openat``
semantics:
- each intermediate component is opened relative to its parent's
descriptor without following symlinks;
- the final component is opened with ``O_NOFOLLOW`` (read, or
``O_WRONLY | O_CREAT | O_EXCL`` when ``write`` is True).
The important detail is that intermediate components are opened with
``O_PATH | O_NOFOLLOW`` when ``O_PATH`` is available, and then verified
with ``fstat()``. On Linux, ``O_RDONLY | O_DIRECTORY | O_NOFOLLOW`` is not
sufficient for this job: a symlink whose target is a directory can still be
opened as the target directory on some kernels. Opening with ``O_PATH`` and
checking the resulting descriptor reliably exposes such a component as a
symlink instead.
A symlink (or a ``..`` component) anywhere in the path raises
``OSError(ELOOP)``. On platforms without ``openat``/``O_DIRECTORY``
support, this falls back to a single ``O_NOFOLLOW`` open of the whole path,
which is no worse than the historical behaviour.
"""
cloexec = getattr(os, "O_CLOEXEC", 0)
nofollow = getattr(os, "O_NOFOLLOW", 0)
o_directory = getattr(os, "O_DIRECTORY", 0)
o_path = getattr(os, "O_PATH", 0)
if write:
final_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | cloexec | nofollow
else:
final_flags = os.O_RDONLY | cloexec | nofollow
supports_openat = bool(
o_directory and nofollow and os.open in getattr(os, "supports_dir_fd", set())
)
if not supports_openat:
return os.open(path, final_flags, mode)
absolute = path.startswith("/")
parts = [p for p in path.split("/") if p not in ("", ".")]
if not parts:
return os.open(path, final_flags, mode)
*parent_parts, leaf = parts
# Use O_PATH for directory descriptors when available. O_PATH descriptors
# can be used as dir_fd anchors for later openat-style calls, and with
# O_NOFOLLOW they let us fstat() a symlink component instead of silently
# following it. If O_PATH is unavailable, use O_RDONLY and an lstat()
# pre-check for intermediate components as a best-effort fallback.
dir_base_flags = (o_path if o_path else os.O_RDONLY) | cloexec | o_directory
component_flags = (
(o_path if o_path else os.O_RDONLY) | cloexec | o_directory | nofollow
)
dir_fd = os.open("/" if absolute else ".", dir_base_flags)
try:
for component in parent_parts:
if component == "..":
raise OSError(errno.ELOOP, "unsafe '..' path component", path)
if not o_path:
# Best-effort fallback for platforms without O_PATH. This is not
# as race-resistant as the descriptor-only path, but it avoids
# known symlink parents where we cannot open the component itself
# as a non-followed O_PATH descriptor.
try:
st = os.lstat(component, dir_fd=dir_fd)
except OSError:
raise
if stat.S_ISLNK(st.st_mode):
raise OSError(errno.ELOOP, "symlinked path component", path)
if not stat.S_ISDIR(st.st_mode):
raise OSError(errno.ENOTDIR, "non-directory path component", path)
try:
next_fd = os.open(component, component_flags, dir_fd=dir_fd)
except OSError as e:
if e.errno in {errno.ELOOP, errno.ENOTDIR}:
try:
st = os.lstat(component, dir_fd=dir_fd)
except OSError:
raise
if stat.S_ISLNK(st.st_mode):
raise OSError(
errno.ELOOP,
"symlinked path component",
path,
) from e
raise
try:
st = os.fstat(next_fd)
if stat.S_ISLNK(st.st_mode):
raise OSError(errno.ELOOP, "symlinked path component", path)
if not stat.S_ISDIR(st.st_mode):
raise OSError(errno.ENOTDIR, "non-directory path component", path)
except Exception:
os.close(next_fd)
raise
os.close(dir_fd)
dir_fd = next_fd
if leaf == "..":
raise OSError(errno.ELOOP, "unsafe '..' path component", path)
return os.open(leaf, final_flags, mode, dir_fd=dir_fd)
finally:
os.close(dir_fd)
def stat_triplet_from_stat(st: os.stat_result) -> Tuple[str, str, str]: def stat_triplet_from_stat(st: os.stat_result) -> Tuple[str, str, str]:
"""Return (owner, group, mode) for an existing stat result.""" """Return (owner, group, mode) for an existing stat result."""

View file

@ -5,12 +5,13 @@ import os
from typing import Dict, List, Optional, Set from typing import Dict, List, Optional, Set
from .. import harvest as h from .. import harvest as h
from ..capture import capture_file from ..capture import capture_file, capture_link
from ..harvest_types import ( from ..harvest_types import (
ExcludedFile, ExcludedFile,
ExtraPathsSnapshot, ExtraPathsSnapshot,
ManagedDir, ManagedDir,
ManagedFile, ManagedFile,
ManagedLink,
UsrLocalCustomSnapshot, UsrLocalCustomSnapshot,
) )
from ..system_paths import MAX_FILES_CAP from ..system_paths import MAX_FILES_CAP
@ -132,6 +133,7 @@ class ExtraPathsCollector(HarvestCollector):
self.notes: List[str] = [] self.notes: List[str] = []
self.excluded: List[ExcludedFile] = [] self.excluded: List[ExcludedFile] = []
self.managed: List[ManagedFile] = [] self.managed: List[ManagedFile] = []
self.managed_links: List[ManagedLink] = []
self.managed_dirs: List[ManagedDir] = [] self.managed_dirs: List[ManagedDir] = []
self.dir_seen: Set[str] = set() self.dir_seen: Set[str] = set()
@ -178,28 +180,53 @@ class ExtraPathsCollector(HarvestCollector):
exclude_patterns=self.exclude_specs, exclude_patterns=self.exclude_specs,
managed_dirs=self.managed_dirs, managed_dirs=self.managed_dirs,
managed_files=self.managed, managed_files=self.managed,
managed_links=self.managed_links,
excluded=self.excluded, excluded=self.excluded,
notes=self.notes, notes=self.notes,
) )
def _collect_included_dirs(self) -> None: def _collect_included_dirs(self) -> None:
role_seen = self.seen_by_role.setdefault(self.role_name, set())
for pat in self.context.path_filter.iter_include_patterns(): for pat in self.context.path_filter.iter_include_patterns():
if pat.kind == "prefix": if pat.kind == "prefix":
path = pat.value path = pat.value
if os.path.isdir(path) and not os.path.islink(path): if os.path.islink(path):
self._walk_and_capture_dirs(path) self._capture_included_link(path, role_seen)
elif os.path.isdir(path):
self._walk_and_capture_dirs(path, role_seen)
elif pat.kind == "glob": elif pat.kind == "glob":
for hit in glob.glob(pat.value, recursive=True): for hit in glob.glob(pat.value, recursive=True):
if os.path.isdir(hit) and not os.path.islink(hit): if os.path.islink(hit):
self._walk_and_capture_dirs(hit) self._capture_included_link(hit, role_seen)
elif os.path.isdir(hit):
self._walk_and_capture_dirs(hit, role_seen)
def _walk_and_capture_dirs(self, root: str) -> None: def _capture_included_link(self, path: str, role_seen: Set[str]) -> None:
path = os.path.normpath(path)
if not path.startswith("/"):
path = "/" + path
if path in self.already_all:
return
if capture_link(
role_name=self.role_name,
abs_path=path,
reason="user_include_link",
policy=self.context.policy,
path_filter=self.context.path_filter,
managed_out=self.managed_links,
excluded_out=self.excluded,
seen_role=role_seen,
seen_global=self.context.captured_global,
):
self.already_all.add(path)
def _walk_and_capture_dirs(self, root: str, role_seen: Set[str]) -> None:
root = os.path.normpath(root) root = os.path.normpath(root)
if not root.startswith("/"): if not root.startswith("/"):
root = "/" + root root = "/" + root
if not os.path.isdir(root) or os.path.islink(root): if not os.path.isdir(root) or os.path.islink(root):
return return
for dirpath, dirnames, _ in os.walk(root, followlinks=False): for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
if len(self.managed_dirs) >= MAX_FILES_CAP: if len(self.managed_dirs) >= MAX_FILES_CAP:
self.notes.append( self.notes.append(
f"Reached directory cap ({MAX_FILES_CAP}) while scanning {root}." f"Reached directory cap ({MAX_FILES_CAP}) while scanning {root}."
@ -243,7 +270,17 @@ class ExtraPathsCollector(HarvestCollector):
pruned: List[str] = [] pruned: List[str] = []
for dirname in dirnames: for dirname in dirnames:
path = os.path.join(dirpath, dirname) path = os.path.join(dirpath, dirname)
if os.path.islink(path) or self.context.path_filter.is_excluded(path): if self.context.path_filter.is_excluded(path):
continue
if os.path.islink(path):
self._capture_included_link(path, role_seen)
continue continue
pruned.append(dirname) pruned.append(dirname)
dirnames[:] = pruned dirnames[:] = pruned
for filename in filenames:
path = os.path.join(dirpath, filename)
if self.context.path_filter.is_excluded(path):
continue
if os.path.islink(path):
self._capture_included_link(path, role_seen)

View file

@ -8,6 +8,8 @@ import stat
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from .fsutil import open_no_follow_path
DEFAULT_DENY_GLOBS = [ DEFAULT_DENY_GLOBS = [
# Common backup copies created by passwd tools (can contain sensitive data) # Common backup copies created by passwd tools (can contain sensitive data)
@ -97,6 +99,34 @@ BLOCK_START = b"/*"
BLOCK_END = b"*/" BLOCK_END = b"*/"
def normalize_for_match(path: str) -> str:
"""Lexically normalize a path string for deny/allow glob matching.
This collapses redundant separators ("//"), resolves "." and ".."
segments, and strips trailing slashes using ``os.path.normpath`` -- a
pure string operation that never touches the filesystem.
It is deliberately NOT ``os.path.realpath``/``Path.resolve``: resolving
symlinks would stat the filesystem and reintroduce a time-of-check /
time-of-use window before the later ``O_NOFOLLOW`` open in
``inspect_file``. The goal here is only to stop a non-canonical *string*
(e.g. "/etc//shadow" or "/etc/foo/../shadow") from slipping past a deny
glob like "/etc/shadow". It is defense-in-depth on top of the no-follow
open, not a load-bearing control by itself.
``normpath`` preserves a leading "//" because POSIX treats it as
implementation-defined; for glob matching we collapse it to a single
leading slash so patterns anchored at "/" still match.
"""
if not path:
return path
normalized = os.path.normpath(path)
if normalized.startswith("//") and not normalized.startswith("///"):
normalized = normalized[1:]
return normalized
@dataclass(frozen=True) @dataclass(frozen=True)
class FileInspection: class FileInspection:
"""Bytes and metadata captured from one safely-opened source file.""" """Bytes and metadata captured from one safely-opened source file."""
@ -145,26 +175,31 @@ class IgnorePolicy:
yield raw yield raw
def _path_deny_reason(self, path: str) -> Optional[str]: def _path_deny_reason(self, path: str) -> Optional[str]:
# Match against a lexically-normalized path so non-canonical spellings
# (e.g. "/etc//shadow", "/etc/foo/../shadow") cannot slip past a deny
# glob. The original path is still what gets opened/recorded.
match_path = normalize_for_match(path)
# Always ignore plain *.log files (rarely useful as config, often noisy). # Always ignore plain *.log files (rarely useful as config, often noisy).
if path.endswith(".log"): if match_path.endswith(".log"):
return "log_file" return "log_file"
# Ignore editor/backup files that end with a trailing tilde. # Ignore editor/backup files that end with a trailing tilde.
if path.endswith("~"): if match_path.endswith("~"):
return "backup_file" return "backup_file"
# Ignore backup shadow files # Ignore backup shadow files
if path.startswith("/etc/") and path.endswith("-"): if match_path.startswith("/etc/") and match_path.endswith("-"):
return "backup_file" return "backup_file"
if not self.dangerous: if not self.dangerous:
for g in self.deny_globs or []: for g in self.deny_globs or []:
if fnmatch.fnmatch(path, g): if fnmatch.fnmatch(match_path, g):
return "denied_path" return "denied_path"
return None return None
def _content_deny_reason(self, path: str, data: bytes) -> Optional[str]: def _content_deny_reason(self, path: str, data: bytes) -> Optional[str]:
if b"\x00" in data: if b"\x00" in data:
match_path = normalize_for_match(path)
for g in self.allow_binary_globs or []: for g in self.allow_binary_globs or []:
if fnmatch.fnmatch(path, g): if fnmatch.fnmatch(match_path, g):
# Binary is acceptable for explicitly-allowed paths. # Binary is acceptable for explicitly-allowed paths.
return None return None
return "binary_like" return "binary_like"
@ -180,26 +215,33 @@ class IgnorePolicy:
def inspect_file(self, path: str) -> tuple[Optional[str], Optional[FileInspection]]: def inspect_file(self, path: str) -> tuple[Optional[str], Optional[FileInspection]]:
"""Safely inspect a regular file and return the exact bytes to copy. """Safely inspect a regular file and return the exact bytes to copy.
The source is opened with O_NOFOLLOW where available, fstat() is taken The source is opened with O_NOFOLLOW on every path component (see
from that file descriptor, and the whole file is read only after the ``fsutil.open_no_follow_path``), fstat() is taken from that file
size cap passes. With the default 256 KiB cap this avoids a memory DoS descriptor, and the whole file is read only after the size cap passes.
while ensuring secret scanning covers every byte that may be copied. With the default 256 KiB cap this avoids a memory DoS while ensuring
secret scanning covers every byte that may be copied.
Opening every component without following symlinks means a regular
file reached through a symlinked *parent* directory is refused with
``symlink_component`` rather than silently captured -- its logical
path would not have matched the deny globs.
""" """
deny = self._path_deny_reason(path) deny = self._path_deny_reason(path)
if deny: if deny:
return deny, None return deny, None
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW
fd: Optional[int] = None fd: Optional[int] = None
try: try:
try: try:
fd = os.open(path, flags) fd = open_no_follow_path(path)
except OSError as e: except OSError as e:
if e.errno in {errno.ELOOP, errno.ENOTDIR}: if e.errno == errno.ELOOP:
# A symlink (or unsafe '..') somewhere in the path. This is
# distinct from "not a regular file" so operators can see
# why a path under a symlinked parent was skipped.
return "symlink_component", None
if e.errno == errno.ENOTDIR:
return "not_regular_file", None return "not_regular_file", None
return "unreadable", None return "unreadable", None
@ -251,8 +293,9 @@ class IgnorePolicy:
No size checks or content scanning are performed for directories. No size checks or content scanning are performed for directories.
""" """
if not self.dangerous: if not self.dangerous:
match_path = normalize_for_match(path)
for g in self.deny_globs or []: for g in self.deny_globs or []:
if fnmatch.fnmatch(path, g): if fnmatch.fnmatch(match_path, g):
return "denied_path" return "denied_path"
try: try:
@ -283,16 +326,17 @@ class IgnorePolicy:
""" """
# Keep the same fast-path filename ignores as deny_reason(). # Keep the same fast-path filename ignores as deny_reason().
if path.endswith(".log"): match_path = normalize_for_match(path)
if match_path.endswith(".log"):
return "log_file" return "log_file"
if path.endswith("~"): if match_path.endswith("~"):
return "backup_file" return "backup_file"
if path.startswith("/etc/") and path.endswith("-"): if match_path.startswith("/etc/") and match_path.endswith("-"):
return "backup_file" return "backup_file"
if not self.dangerous: if not self.dangerous:
for g in self.deny_globs or []: for g in self.deny_globs or []:
if fnmatch.fnmatch(path, g): if fnmatch.fnmatch(match_path, g):
return "denied_path" return "denied_path"
try: try:

254
poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. # This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand.
[[package]] [[package]]
name = "attrs" name = "attrs"
@ -6,6 +6,7 @@ version = "26.1.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"]
files = [ files = [
{file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"},
{file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"},
@ -17,6 +18,7 @@ version = "5.0.0"
description = "Modern password hashing for your software and your servers" description = "Modern password hashing for your software and your servers"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
files = [ files = [
{file = "bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be"}, {file = "bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be"},
{file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2"}, {file = "bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2"},
@ -93,6 +95,7 @@ version = "2026.6.17"
description = "Python package for providing Mozilla's CA Bundle." description = "Python package for providing Mozilla's CA Bundle."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["appimage"]
files = [ files = [
{file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"},
{file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"},
@ -104,6 +107,8 @@ version = "2.0.0"
description = "Foreign Function Interface for Python calling C code." description = "Foreign Function Interface for Python calling C code."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"]
markers = "platform_python_implementation != \"PyPy\""
files = [ files = [
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
@ -200,6 +205,7 @@ version = "3.4.7"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["appimage"]
files = [ files = [
{file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"},
{file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"},
@ -338,6 +344,8 @@ version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 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 = [ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@ -345,124 +353,110 @@ files = [
[[package]] [[package]]
name = "coverage" name = "coverage"
version = "7.14.1" version = "7.14.2"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["dev"]
files = [ files = [
{file = "coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf"}, {file = "coverage-7.14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b75818e3046e9319143157f3dc4b43679a550c2060a17cbf3e39cc0b552925"},
{file = "coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf"}, {file = "coverage-7.14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66b08ba4c5cbf0eaa2e9692b203073f198d5d469d8b15d1c7a4854ce7032b2e2"},
{file = "coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d"}, {file = "coverage-7.14.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:70f266b536c590060b707dddfb6cf9f17e24fd30b992242e774543d256265c43"},
{file = "coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2"}, {file = "coverage-7.14.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb40cac5b1a6378fdccc99268f1033112ee4636e4fd9aaf240f6930d1fcea12c"},
{file = "coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47"}, {file = "coverage-7.14.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c301fe9990cb5c081bf4881cb498743807c8e0e93fad7b85c02788456492ef8"},
{file = "coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550"}, {file = "coverage-7.14.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d67b0462c8a3c3d93033e7c79cacdfc57d08e5220d9115bcb24a23edf5a5900d"},
{file = "coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e"}, {file = "coverage-7.14.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e763087828ee9644f0c89c57f9b75f0a50fdf3e8f5d8fac5cfc351337e89a99"},
{file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f"}, {file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6d4da2baab6d96ceedd9176b3c142e1198b0310bc8dc04e18a3caab65c3a322c"},
{file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1"}, {file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ab565a405bfdea61260145d8cc987aa66d1998fd0e0ccd4348008f4e6a39ee33"},
{file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5"}, {file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c13230b688fbb9122251b74daa092175811eb64cb7bd1c98e2c8193dfa2b0bd5"},
{file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b"}, {file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:014c83ba1ec97993cfe94e77fe6b56daa76bc0c218b86938971574c28942d044"},
{file = "coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332"}, {file = "coverage-7.14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6caf54ffbf84b30470a8118f275afee9234e616572e4e41bae1dc19198c37294"},
{file = "coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59"}, {file = "coverage-7.14.2-cp310-cp310-win32.whl", hash = "sha256:4bf9d8a35f77df5638c61b5012ba5225109ec1cc15bc5eb097036b3c3cc939f3"},
{file = "coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253"}, {file = "coverage-7.14.2-cp310-cp310-win_amd64.whl", hash = "sha256:c1f17a8caebe0facd4556b1e0adfe0987c17feebed88e7bb6b5365c45c84c5d6"},
{file = "coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f"}, {file = "coverage-7.14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909f265c8c41f04c824bf741b2601fdcb56cab4bf56e018996b6494192ba0f58"},
{file = "coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4"}, {file = "coverage-7.14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8102deaf911938233f760426e6a5e287388521de95111d5c8de26c8a1028924"},
{file = "coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1"}, {file = "coverage-7.14.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:851f49e7bd7d1cdaf328f3133942b252d5e3d3380690131f423cba8e435b87f5"},
{file = "coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f"}, {file = "coverage-7.14.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04cb445bed86aaf00aaa97d41a8b6e30f100f21e81c34caaec4efc684cb57768"},
{file = "coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129"}, {file = "coverage-7.14.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7471bc920d97c51c37ea8127f13b2adca43c3d78c53313b26a1f428e99d2c254"},
{file = "coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860"}, {file = "coverage-7.14.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da5057e1bb257c967feee8ba67f3ebf379e801c7717f238b3d8c9caf00fc8f93"},
{file = "coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c"}, {file = "coverage-7.14.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33c0da852e8a40246cd8e20cf3b2fc17ca52a45e9b5f7983c93db26f5d24b87b"},
{file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7"}, {file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f48a85bb437fab7782021c40bfee6b15146928b96960d008ace41b6901a0f21d"},
{file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec"}, {file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f44e7579a769a21d5b5e3166916bfe30ee175aaffff750324cbb11be2dbec5ad"},
{file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef"}, {file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:78853ca3c6ca2f012daa2b07dbabbb8db0f09d4dbe8ee828d294b3445d3f4cd8"},
{file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df"}, {file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c9c2795ee3692097ff226ab806005d36bb9691fca9b35353542b57ea749cc830"},
{file = "coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9"}, {file = "coverage-7.14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2f5cc48a845d755b6db236f8c29c2b54773eb4c7e4ee2ead43812d73718784b0"},
{file = "coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548"}, {file = "coverage-7.14.2-cp311-cp311-win32.whl", hash = "sha256:9c61cb7eaabcfa609c5bc0f5ff5869d72a2f02f17994e5fba5f971de516f3c82"},
{file = "coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e"}, {file = "coverage-7.14.2-cp311-cp311-win_amd64.whl", hash = "sha256:e715909b0966d1774d8a26e14e2f4a3ae75909dca526901c6306286b2dcbfbdc"},
{file = "coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3"}, {file = "coverage-7.14.2-cp311-cp311-win_arm64.whl", hash = "sha256:9193f7150937a4fd836b10eaa123e15d98e961d1fabac07e60adf2d4785f888a"},
{file = "coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c"}, {file = "coverage-7.14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:37c94712e533ea06f0b1e4d934811c520b1914ce0e4da3916220717aa7a86bc6"},
{file = "coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c"}, {file = "coverage-7.14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c050bbc7bba94c77e4ed7438f4fda1babe98ab145691d80aa6f60df934a1468b"},
{file = "coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b"}, {file = "coverage-7.14.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a7af571767a2ee342a171c16fc1b1a07a0bf511606d381703fb7cf397fe49d46"},
{file = "coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6"}, {file = "coverage-7.14.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8b4910cce599cd2438f8da65f5ef199a70a1cdb6ab314926df78271ca5954240"},
{file = "coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37"}, {file = "coverage-7.14.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c33e9e4878972f430b0cc06de3bf2a28d054a9efb4f8426d27de0d9cb81396ff"},
{file = "coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad"}, {file = "coverage-7.14.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7967ea55c6dea6becba4d5870e2fa0aa4915a8be7ebff1bb79e6207aa75ce8d"},
{file = "coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84"}, {file = "coverage-7.14.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1322f237c2979b84096f4239c17828ff17fea6b3bbe96c44381c5f587c44c26"},
{file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54"}, {file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:77849525340c99f516d793dddbcee16b18d50af892ac43c8de1a6f343d41e3b5"},
{file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7"}, {file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef11695493ec3f06f7b2678ca274bcabb4ca04057317df268ddbfd8b05f661a8"},
{file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9"}, {file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8134f0e0723e080d1c27bbe8fc149f0162e429fa1852482150015d0fce83eaf1"},
{file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02"}, {file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:914eead2b843fc357f733b3fe39cc94f1b53d466e8cfe03080b1ed9d24ccfc73"},
{file = "coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a"}, {file = "coverage-7.14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e4b2d5e847fb7958583b74910cc19e5ec4ece514487385677b26433b2546116e"},
{file = "coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1"}, {file = "coverage-7.14.2-cp312-cp312-win32.whl", hash = "sha256:e753db9e40dda7302e0ac3e1e6e1325fb7f7b4694f87a7314ab15dd5d57911a7"},
{file = "coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e"}, {file = "coverage-7.14.2-cp312-cp312-win_amd64.whl", hash = "sha256:d32e5ca5f16dafb269ee50b60d32b00c704b3f6f78e238105f1d94a3a5f24bf5"},
{file = "coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a"}, {file = "coverage-7.14.2-cp312-cp312-win_arm64.whl", hash = "sha256:dc366f158e2fb2add9d4e57338ca48f12611024278688ee657eb0b853fcb5de5"},
{file = "coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793"}, {file = "coverage-7.14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e5f077641a6713ce9d38df9e85d4fb9e008677fc0775cbaeb32ddfc3b319d4ca"},
{file = "coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d"}, {file = "coverage-7.14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0907f39b49ae818fe8af50aaa0f19afbc8ca164aea0865181ca7af17a3ac690b"},
{file = "coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247"}, {file = "coverage-7.14.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734d47669118d75c28981e562d4530ceb77342d31ffef6def5edd5ad4f05d7b"},
{file = "coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d"}, {file = "coverage-7.14.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d9a1b5813d00ea6151f6ccf64d1fa16892771dfdda12ba87162d15ec4ea3e1e"},
{file = "coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b"}, {file = "coverage-7.14.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f0a80f4c8ac3f774210b1cc1bc0e31e75502f2818dda9a144ff90e702c4d91d"},
{file = "coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be"}, {file = "coverage-7.14.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e66f3f22d6c1515ce70f2e7c3e9c6f3ff0ff33480125c9f9c53e8f6508e30f"},
{file = "coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43"}, {file = "coverage-7.14.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6a2c37c3114f87ca7f10113756026eecb49656514debad600dcbec21f355ccea"},
{file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901"}, {file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b16a7959d04b1497281c062c180413565c3f3469211d78799ad5b9a75f67796"},
{file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff"}, {file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6466c6999545cf00c4c142dfcbbf2db396dc735f005dcf8f91d57e351a79472b"},
{file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4"}, {file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c60915ebb8f562317ba5ff6b8c32e25c0882289b201a9f2fb2987f91efd95d8"},
{file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d"}, {file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:33b830850488acbcd358c78a4fecfafe7031667b4da8ddff5546295dc962cdeb"},
{file = "coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33"}, {file = "coverage-7.14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d0f845539230b8269aec902bc978b0cc403f52f002d18a04492efc943404d0bc"},
{file = "coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c"}, {file = "coverage-7.14.2-cp313-cp313-win32.whl", hash = "sha256:a8ac51a2e441e9119b9395f4d893fbc4934c64c8ba58be9b9eaa85591249e548"},
{file = "coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416"}, {file = "coverage-7.14.2-cp313-cp313-win_amd64.whl", hash = "sha256:039b264cdb31c44b48f9821e2afbf8f37df49e0fb837e24a942918b36c567e31"},
{file = "coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42"}, {file = "coverage-7.14.2-cp313-cp313-win_arm64.whl", hash = "sha256:7f2ef591e381cc36b8e53334e1b842c760c520c8a52d01e8626209400e93fe6a"},
{file = "coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d"}, {file = "coverage-7.14.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7a0d1f026b72d627fa5c8a57cbc86ad209b64aa2a65833c83b290ace5cbee126"},
{file = "coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5"}, {file = "coverage-7.14.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4d2b86f81c1c9310a7e774e3cc9e927a3d0bf583ecbfa01498dd626930025428"},
{file = "coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52"}, {file = "coverage-7.14.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d76bdc1f9396ae70a55d050cf9743d88141c62ce0a22a3f627fab1d11c2f8bc6"},
{file = "coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a"}, {file = "coverage-7.14.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64"},
{file = "coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a"}, {file = "coverage-7.14.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0904f3b79d7b845bef0715afe1900da634d12b97f05b9479cb472880ca07cb9c"},
{file = "coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2"}, {file = "coverage-7.14.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6795ca4198d6cb7fc2c6163214f6555a6bc5f0ae1e268e76139dec4b37c4499"},
{file = "coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e"}, {file = "coverage-7.14.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c41e9b60fc0fa57f5d73306417d2f9d668202cca6944f9435878c55a5e7ae213"},
{file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d"}, {file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419d2aadd5746efc2e9df0f33c05570d8192e6f6a6098ab05acce586f44ce8a5"},
{file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb"}, {file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c5d273c5f1411c0d26c4f066c398d4a434b1f97bb5fa409189bedce86d4add4"},
{file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d"}, {file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5fe465bc691264adce601527a972990c1174075d86bcbe9968fd20c95e0b1948"},
{file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69"}, {file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6fbb61617af1c56f95d53170ae9fa6c9aef6de1abd02fcc50064bfc672efb18d"},
{file = "coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54"}, {file = "coverage-7.14.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e1eff22b831dfd5694989cc1f0789980f18391f614ac67c851af9a8e6d25e9ba"},
{file = "coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1"}, {file = "coverage-7.14.2-cp314-cp314-win32.whl", hash = "sha256:58e91be0a233adef698d3e6be54f10401bb91fd7854c0d4c4d50e0d3711e72f1"},
{file = "coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce"}, {file = "coverage-7.14.2-cp314-cp314-win_amd64.whl", hash = "sha256:d8429bf97906bfe6c61f9dbfb3342e0d88120da61939da8bd04f830cc3eab3b8"},
{file = "coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1"}, {file = "coverage-7.14.2-cp314-cp314-win_arm64.whl", hash = "sha256:13609d9d77249447aa73357b14831b0f3b95f275026c9ff20dd105f981f53a0c"},
{file = "coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee"}, {file = "coverage-7.14.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9818486c2bac88ae931df7e04905ee29bef49fd218c00f5f02bed4855254a101"},
{file = "coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500"}, {file = "coverage-7.14.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:58055adffabfa243516a197aa9f85f0dd56d905b0fba1a10193269759c29ccb0"},
{file = "coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906"}, {file = "coverage-7.14.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:535747dbc200349d7fb434cffcb28e770f0290f69b225f56dc3803aa7210cdea"},
{file = "coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42"}, {file = "coverage-7.14.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:420c66e35d85c0ca5dc6a38147d83ef239762542900e5921ebbdb89333c540ea"},
{file = "coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8"}, {file = "coverage-7.14.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2cf17b33773be446a588551ea6a746b2d70dd0bc90dc31f1dd7648975a63c6b"},
{file = "coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851"}, {file = "coverage-7.14.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:adb4a5fef041f7179bb264203add873c147d169cf2f8d0adae89ff2e51271bac"},
{file = "coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034"}, {file = "coverage-7.14.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c012ec357dec9408a83dad5541172a63c5cfa1421709f2e5811480d31ae1b28"},
{file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c"}, {file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dacd0ecd08fda3cb2f85b60cabea7da326dcb2fc15fbb23a88830a80144cc9f2"},
{file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36"}, {file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f27e980f2feba5dfe7a32b22b125470de69c0bd113c75e16165de909a777f512"},
{file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5"}, {file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:105c00efb65c863630b2b63cbf7b8267e4da2d44b62284efbb19a03b04c337d4"},
{file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4"}, {file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:571173fa04c8e8d6235ab32ae67fecca97777e2e1b4a1a30f3022c34e397c1c1"},
{file = "coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d"}, {file = "coverage-7.14.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e532f34d42d1a421fa00ed6b7735d14ac2e340256c1bad26a5e1dc1252b0bed7"},
{file = "coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee"}, {file = "coverage-7.14.2-cp314-cp314t-win32.whl", hash = "sha256:243971550fb46c3039257f75e65610002d84304c505f609bbd9779e20a653a0a"},
{file = "coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7"}, {file = "coverage-7.14.2-cp314-cp314t-win_amd64.whl", hash = "sha256:60fb0ca084a92da96474b8b405a7ea76dfecac3c68db54383e7934b6f3871169"},
{file = "coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343"}, {file = "coverage-7.14.2-cp314-cp314t-win_arm64.whl", hash = "sha256:36a0a3f42ed7dfdbca2a69a541519ffd5064a5692152fc0018109e74370d7345"},
{file = "coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1"}, {file = "coverage-7.14.2-py3-none-any.whl", hash = "sha256:04d92589e481a8b68a005a5a1e0646a91c76f322c397c4635298c57cf63699b5"},
{file = "coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b"}, {file = "coverage-7.14.2.tar.gz", hash = "sha256:7a2da3d81cfe17c18038c6d98e6592aa9147d596d056119b0ee612c3c8bd5230"},
{file = "coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474"},
{file = "coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86"},
{file = "coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e"},
{file = "coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65"},
{file = "coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e"},
{file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8"},
{file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07"},
{file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de"},
{file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890"},
{file = "coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd"},
{file = "coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e"},
{file = "coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c"},
{file = "coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af"},
{file = "coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2"},
{file = "coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be"},
] ]
[package.dependencies] [package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras] [package.extras]
toml = ["tomli"] toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
@ -470,6 +464,7 @@ version = "49.0.0"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false optional = false
python-versions = "!=3.9.0,!=3.9.1,>=3.9" python-versions = "!=3.9.0,!=3.9.1,>=3.9"
groups = ["main"]
files = [ files = [
{file = "cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db"}, {file = "cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db"},
{file = "cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db"}, {file = "cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db"},
@ -521,7 +516,7 @@ files = [
[package.dependencies] [package.dependencies]
cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""} cffi = {version = ">=2.0.0", markers = "platform_python_implementation != \"PyPy\""}
typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11\""} typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""}
[package.extras] [package.extras]
ssh = ["bcrypt (>=3.1.5)"] ssh = ["bcrypt (>=3.1.5)"]
@ -532,6 +527,7 @@ version = "5.0"
description = "A library for working with .desktop files" description = "A library for working with .desktop files"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["appimage"]
files = [ files = [
{file = "desktop_entry_lib-5.0-py3-none-any.whl", hash = "sha256:e60a0c2c5e42492dbe5378e596b1de87d1b1c4dc74d1f41998a164ee27a1226f"}, {file = "desktop_entry_lib-5.0-py3-none-any.whl", hash = "sha256:e60a0c2c5e42492dbe5378e596b1de87d1b1c4dc74d1f41998a164ee27a1226f"},
{file = "desktop_entry_lib-5.0.tar.gz", hash = "sha256:9a621bac1819fe21021356e41fec0ac096ed56e6eb5dcfe0639cd8654914b864"}, {file = "desktop_entry_lib-5.0.tar.gz", hash = "sha256:9a621bac1819fe21021356e41fec0ac096ed56e6eb5dcfe0639cd8654914b864"},
@ -546,6 +542,8 @@ version = "1.3.1"
description = "Backport of PEP 654 (exception groups)" description = "Backport of PEP 654 (exception groups)"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version == \"3.10\""
files = [ files = [
{file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"},
{file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"},
@ -563,6 +561,7 @@ version = "3.18"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["appimage"]
files = [ files = [
{file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"},
{file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"},
@ -577,6 +576,7 @@ version = "2.3.0"
description = "brain-dead simple config-ini parsing" description = "brain-dead simple config-ini parsing"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["dev"]
files = [ files = [
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
@ -588,6 +588,7 @@ version = "3.0.3"
description = "Pythonic task execution" description = "Pythonic task execution"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"]
files = [ files = [
{file = "invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053"}, {file = "invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053"},
{file = "invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c"}, {file = "invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c"},
@ -599,6 +600,7 @@ version = "4.26.0"
description = "An implementation of JSON Schema validation for Python" description = "An implementation of JSON Schema validation for Python"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main"]
files = [ files = [
{file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"},
{file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"},
@ -606,7 +608,7 @@ files = [
[package.dependencies] [package.dependencies]
attrs = ">=22.2.0" attrs = ">=22.2.0"
jsonschema-specifications = ">=2023.03.6" jsonschema-specifications = ">=2023.3.6"
referencing = ">=0.28.4" referencing = ">=0.28.4"
rpds-py = ">=0.25.0" rpds-py = ">=0.25.0"
@ -620,6 +622,7 @@ version = "2025.9.1"
description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"]
files = [ files = [
{file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"},
{file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"},
@ -634,6 +637,7 @@ version = "26.2"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["dev"]
files = [ files = [
{file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
{file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
@ -645,6 +649,7 @@ version = "5.0.0"
description = "SSH2 protocol library" description = "SSH2 protocol library"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"]
files = [ files = [
{file = "paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c"}, {file = "paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c"},
{file = "paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79"}, {file = "paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79"},
@ -662,6 +667,7 @@ version = "1.6.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["dev"]
files = [ files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
@ -677,6 +683,8 @@ version = "3.0"
description = "C parser in Python" description = "C parser in Python"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main"]
markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
files = [ files = [
{file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
{file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
@ -688,6 +696,7 @@ version = "2.20.0"
description = "Pygments is a syntax highlighting package written in Python." description = "Pygments is a syntax highlighting package written in Python."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["dev"]
files = [ files = [
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
@ -702,6 +711,7 @@ version = "1.6.2"
description = "Python binding to the Networking and Cryptography (NaCl) library" description = "Python binding to the Networking and Cryptography (NaCl) library"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
files = [ files = [
{file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"}, {file = "pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594"},
{file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"}, {file = "pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0"},
@ -743,6 +753,7 @@ version = "4.2"
description = "Generate AppImages from your Python projects" description = "Generate AppImages from your Python projects"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["appimage"]
files = [ files = [
{file = "pyproject_appimage-4.2-py3-none-any.whl", hash = "sha256:d6892643db5759dc06531a4546bdab404a519c63814c060f8749979a8625d9cc"}, {file = "pyproject_appimage-4.2-py3-none-any.whl", hash = "sha256:d6892643db5759dc06531a4546bdab404a519c63814c060f8749979a8625d9cc"},
{file = "pyproject_appimage-4.2.tar.gz", hash = "sha256:6b6387250cb1e6ecbb08a13f5810749396ebe8637f2f35bf2296bfdd5e65cd6e"}, {file = "pyproject_appimage-4.2.tar.gz", hash = "sha256:6b6387250cb1e6ecbb08a13f5810749396ebe8637f2f35bf2296bfdd5e65cd6e"},
@ -759,6 +770,7 @@ version = "8.4.2"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["dev"]
files = [ files = [
{file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"},
{file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"},
@ -782,6 +794,7 @@ version = "5.0.0"
description = "Pytest plugin for measuring coverage." description = "Pytest plugin for measuring coverage."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["dev"]
files = [ files = [
{file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
{file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
@ -800,6 +813,7 @@ version = "6.0.3"
description = "YAML parser and emitter for Python" description = "YAML parser and emitter for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
files = [ files = [
{file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {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_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
@ -882,6 +896,7 @@ version = "0.37.0"
description = "JSON Referencing + Python" description = "JSON Referencing + Python"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main"]
files = [ files = [
{file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"},
{file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"},
@ -898,6 +913,7 @@ version = "2.34.2"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["appimage"]
files = [ files = [
{file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"},
{file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"},
@ -919,6 +935,7 @@ version = "0.30.0"
description = "Python bindings to Rust's persistent data structures (rpds)" description = "Python bindings to Rust's persistent data structures (rpds)"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main"]
files = [ files = [
{file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"},
{file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"},
@ -1043,6 +1060,7 @@ version = "2.4.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["appimage", "dev"]
files = [ 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_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-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"},
@ -1092,6 +1110,7 @@ files = [
{file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"},
{file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"},
] ]
markers = {appimage = "python_version == \"3.10\"", dev = "python_full_version <= \"3.11.0a6\""}
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
@ -1099,10 +1118,12 @@ version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+" description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main", "dev"]
files = [ files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
] ]
markers = {main = "python_version < \"3.13\"", dev = "python_version == \"3.10\""}
[[package]] [[package]]
name = "urllib3" name = "urllib3"
@ -1110,18 +1131,19 @@ version = "2.7.0"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["appimage"]
files = [ files = [
{file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
{file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
] ]
[package.extras] [package.extras]
brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"] brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"] h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["backports-zstd (>=1.0.0)"] zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.1"
python-versions = "^3.10" python-versions = ">=3.10"
content-hash = "30e16396439f2cdd69005a5b7bdf8144aac33422a77a63accbc9eaa74151d851" content-hash = "84ca97970dccb13020927ea329bab59d43e8e8c954befe06c8048caa3cc8020b"

View file

@ -1,34 +1,51 @@
[tool.poetry] [build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[project]
name = "enroll" name = "enroll"
version = "0.7.0b7" version = "0.7.0b7"
description = "Enroll a server's running state retrospectively into Ansible, Puppet or Salt" description = "Enroll a server's running state retrospectively into Ansible, Puppet or Salt"
authors = ["Miguel Jacq <mig@mig5.net>"]
license = "GPL-3.0-or-later"
readme = "README.md" readme = "README.md"
packages = [{ include = "enroll" }] requires-python = ">=3.10"
repository = "https://git.mig5.net/mig5/enroll" license = "GPL-3.0-or-later"
include = [ authors = [
{ path = "enroll/schema/state.schema.json", format = ["sdist", "wheel"] } { name = "Miguel Jacq", email = "mig@mig5.net" },
]
dependencies = [
"PyYAML>=6,<7",
"paramiko>=3.5",
"jsonschema>=4.23,<5",
] ]
[tool.poetry.dependencies] [project.urls]
python = "^3.10" Repository = "https://git.mig5.net/mig5/enroll"
pyyaml = "^6"
paramiko = ">=3.5"
jsonschema = "^4.23.0"
[tool.poetry.scripts] [project.scripts]
enroll = "enroll.cli:main" enroll = "enroll.cli:main"
[build-system] [dependency-groups]
requires = ["poetry-core>=1.8.0"] dev = [
build-backend = "poetry.core.masonry.api" "pytest>=8,<9",
"pytest-cov>=5,<6",
]
appimage = [
"pyproject-appimage>=4.2,<5",
]
[tool.poetry]
requires-poetry = ">=2.0"
packages = [{ include = "enroll" }]
include = [
{ path = "enroll/schema/state.schema.json", format = ["sdist", "wheel"] },
]
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.appimage]
optional = true
[tool.pyproject-appimage] [tool.pyproject-appimage]
script = "enroll" script = "enroll"
output = "Enroll.AppImage" output = "Enroll.AppImage"
[tool.poetry.dev-dependencies]
pytest = "^8"
pytest-cov = "^5"
pyproject-appimage = "^4.2"

View file

@ -2,4 +2,4 @@
set -eou pipefail set -eou pipefail
poetry run pytest -q tests -vvv --cov=enroll --cov-report=term-missing poetry run python -m pytest -q tests -vvv --cov=enroll --cov-report=term-missing

View file

@ -330,7 +330,7 @@ ensure_salt() {
run_pytests() { run_pytests() {
section "Python unit tests" section "Python unit tests"
cd "${PROJECT_ROOT}" cd "${PROJECT_ROOT}"
run poetry run pytest -vvvv --cov=enroll --cov-report=term-missing --disable-warnings run poetry run python -m pytest -vvvv --cov=enroll --cov-report=term-missing --disable-warnings
} }
prepare_harvest_fixture() { prepare_harvest_fixture() {

View file

@ -221,6 +221,25 @@ def test_find_user_ssh_files_ignores_symlink(tmp_path: Path):
assert result == [] assert result == []
def test_find_user_ssh_files_ignores_symlinked_ssh_dir(tmp_path: Path):
"""A user who replaces ~/.ssh with a symlink to a sensitive directory must
not have files inside it harvested through the symlinked parent. os.path.isdir
follows symlinks, so the directory itself must be checked with islink().
"""
from enroll.accounts import find_user_ssh_files
sensitive = tmp_path / "sensitive"
sensitive.mkdir()
(sensitive / "authorized_keys").write_text("ssh-rsa AAAA...\n", encoding="utf-8")
home = tmp_path / "home" / "mallory"
home.mkdir(parents=True)
os.symlink(str(sensitive), str(home / ".ssh"))
assert find_user_ssh_files(str(home)) == []
def test_find_user_ssh_files_handles_home_not_starting_with_slash(): def test_find_user_ssh_files_handles_home_not_starting_with_slash():
from enroll.accounts import find_user_ssh_files from enroll.accounts import find_user_ssh_files

View file

@ -23,3 +23,54 @@ def test_stat_triplet_reports_mode(tmp_path: Path):
assert mode == "0600" assert mode == "0600"
assert owner # non-empty string assert owner # non-empty string
assert group # non-empty string assert group # non-empty string
def test_open_no_follow_path_reads_regular_file(tmp_path: Path):
from enroll.fsutil import open_no_follow_path
nested = tmp_path / "a" / "b"
nested.mkdir(parents=True)
f = nested / "file.txt"
f.write_text("hello\n", encoding="utf-8")
fd = open_no_follow_path(str(f))
try:
assert os.read(fd, 100) == b"hello\n"
finally:
os.close(fd)
def test_open_no_follow_path_refuses_symlinked_parent(tmp_path: Path):
import errno
from enroll.fsutil import open_no_follow_path
real = tmp_path / "real"
real.mkdir()
(real / "file.txt").write_text("x\n", encoding="utf-8")
(tmp_path / "link").symlink_to(real)
try:
fd = open_no_follow_path(str(tmp_path / "link" / "file.txt"))
os.close(fd)
raise AssertionError("expected OSError for symlinked parent")
except OSError as e:
assert e.errno == errno.ELOOP
def test_open_no_follow_path_refuses_symlinked_leaf(tmp_path: Path):
import errno
from enroll.fsutil import open_no_follow_path
target = tmp_path / "target.txt"
target.write_text("x\n", encoding="utf-8")
link = tmp_path / "link.txt"
link.symlink_to(target)
try:
fd = open_no_follow_path(str(link))
os.close(fd)
raise AssertionError("expected OSError for symlinked leaf")
except OSError as e:
assert e.errno == errno.ELOOP

View file

@ -394,3 +394,53 @@ def test_usr_local_custom_collector_scans_executable_bin_and_notes_cap(
"usr_local_etc_custom", "usr_local_etc_custom",
"usr_local_bin_script", "usr_local_bin_script",
] ]
def test_extra_paths_collector_records_symlinks_without_following(tmp_path):
root = tmp_path / "include"
root.mkdir()
real_file = root / "real.conf"
real_file.write_text("ok", encoding="utf-8")
(root / "link.conf").symlink_to("real.conf")
outside = tmp_path / "outside"
outside.mkdir()
(outside / "outside.conf").write_text("do-not-follow", encoding="utf-8")
(root / "shared").symlink_to(outside, target_is_directory=True)
ctx = _context(tmp_path, include=[str(root)])
result = ExtraPathsCollector(
ctx,
seen_by_role={},
already_all=set(),
include_paths=[str(root)],
).collect()
links = {(link.path, link.target, link.reason) for link in result.managed_links}
assert (str(root / "link.conf"), "real.conf", "user_include_link") in links
assert (str(root / "shared"), str(outside), "user_include_link") in links
managed_files = {mf.path for mf in result.managed_files}
assert str(real_file) in managed_files
assert str(outside / "outside.conf") not in managed_files
def test_extra_paths_collector_records_include_path_that_is_symlink(tmp_path):
real_root = tmp_path / "real"
real_root.mkdir()
(real_root / "inside.conf").write_text("do-not-follow", encoding="utf-8")
link_root = tmp_path / "linked-root"
link_root.symlink_to(real_root, target_is_directory=True)
ctx = _context(tmp_path, include=[str(link_root)])
result = ExtraPathsCollector(
ctx,
seen_by_role={},
already_all=set(),
include_paths=[str(link_root)],
).collect()
assert [(link.path, link.target, link.reason) for link in result.managed_links] == [
(str(link_root), str(real_root), "user_include_link")
]
assert result.managed_files == []

View file

@ -102,7 +102,45 @@ def test_capture_file_rejects_symlink_source_with_ignore_policy(tmp_path: Path):
assert ok is False assert ok is False
assert managed == [] assert managed == []
assert excluded and excluded[0].reason == "not_regular_file" # Symlinked sources are now reported with the dedicated symlink_component
# reason (covers both symlinked leaves and symlinked parent directories),
# which is more precise than the old generic not_regular_file.
assert excluded and excluded[0].reason == "symlink_component"
def test_capture_file_rejects_symlinked_parent_with_ignore_policy(tmp_path: Path):
"""O_NOFOLLOW only guards the final component. A regular file reached
through a symlinked *parent* directory must still be refused, otherwise a
file whose real location is deny-globbed could be captured while its
logical (recorded) path looks safe.
"""
secret = tmp_path / "secretroot"
secret.mkdir()
(secret / "config").write_text("listen_port=8080\n", encoding="utf-8")
(tmp_path / "allowed").symlink_to(secret, target_is_directory=True)
bundle = tmp_path / "bundle"
bundle.mkdir()
managed: list[ManagedFile] = []
excluded: list[ExcludedFile] = []
ok = capture_file(
bundle_dir=str(bundle),
role_name="role",
abs_path=str(tmp_path / "allowed" / "config"),
reason="test",
policy=IgnorePolicy(),
path_filter=PathFilter(),
managed_out=managed,
excluded_out=excluded,
)
assert ok is False
assert managed == []
assert excluded and excluded[0].reason == "symlink_component"
# Nothing should have been written into the bundle.
artifact = bundle / "artifacts" / "role" / "allowed" / "config"
assert not artifact.exists()
def test_prepare_new_private_dir_rejects_symlink_parent(tmp_path: Path): def test_prepare_new_private_dir_rejects_symlink_parent(tmp_path: Path):

View file

@ -236,7 +236,10 @@ def test_deny_reason_symlink_file(tmp_path: Path):
link = tmp_path / "link" link = tmp_path / "link"
os.symlink(str(real_file), str(link)) os.symlink(str(real_file), str(link))
reason = pol.deny_reason(str(link)) reason = pol.deny_reason(str(link))
assert reason == "not_regular_file" # A symlinked path (final component or parent) is refused with the
# dedicated symlink_component reason so operators can tell symlink
# redirection apart from genuine non-regular files (sockets, devices).
assert reason == "symlink_component"
def test_deny_reason_logs(tmp_path: Path): def test_deny_reason_logs(tmp_path: Path):
@ -306,3 +309,110 @@ def test_secret_scan_reads_whole_file_under_size_cap(tmp_path):
p = tmp_path / "large.conf" p = tmp_path / "large.conf"
p.write_bytes(b"A" * 70_000 + b"\nlate_token = abc123\n") p.write_bytes(b"A" * 70_000 + b"\nlate_token = abc123\n")
assert IgnorePolicy().deny_reason(str(p)) == "sensitive_content" assert IgnorePolicy().deny_reason(str(p)) == "sensitive_content"
def test_normalize_for_match_collapses_noncanonical_paths():
from enroll.ignore import normalize_for_match
assert normalize_for_match("/etc/shadow") == "/etc/shadow"
assert normalize_for_match("/etc//shadow") == "/etc/shadow"
assert normalize_for_match("/etc/foo/../shadow") == "/etc/shadow"
assert normalize_for_match("/etc/./shadow") == "/etc/shadow"
assert normalize_for_match("/etc/shadow/") == "/etc/shadow"
# A leading "//" is POSIX-significant to normpath but must collapse for
# glob matching anchored at "/".
assert normalize_for_match("//etc/shadow") == "/etc/shadow"
# "///" collapses to "/" via normpath already; ensure we don't mangle it.
assert normalize_for_match("///etc/shadow") == "/etc/shadow"
# Empty stays empty (no crash).
assert normalize_for_match("") == ""
def test_deny_reason_denies_noncanonical_sensitive_paths():
# Regression: non-canonical spellings of a denied path must still be denied
# rather than slipping past the deny glob. Defense-in-depth on top of the
# O_NOFOLLOW open in inspect_file(); see normalize_for_match().
pol = IgnorePolicy()
assert pol._path_deny_reason("/etc//shadow") == "denied_path"
assert pol._path_deny_reason("/etc/foo/../shadow") == "denied_path"
assert pol._path_deny_reason("/etc/./shadow") == "denied_path"
assert pol._path_deny_reason("/etc/ssl/private/../private/key") == "denied_path"
assert pol._path_deny_reason("//etc/shadow") == "denied_path"
# A normal config path is unaffected.
assert pol._path_deny_reason("/etc/nginx/nginx.conf") is None
def test_deny_reason_dir_denies_noncanonical_sensitive_paths():
pol = IgnorePolicy()
# normpath("/etc/ssl/private/../private") -> "/etc/ssl/private" which is the
# glob root itself, so use paths that still resolve to a child of it.
assert pol.deny_reason_dir("/etc/ssl/private/sub/../child") == "denied_path"
assert pol.deny_reason_dir("/etc//ssl/private/sub") == "denied_path"
def test_deny_reason_link_denies_noncanonical_sensitive_paths():
pol = IgnorePolicy()
assert pol.deny_reason_link("/etc/ssh/../ssh/ssh_host_rsa_key") == "denied_path"
assert pol.deny_reason_link("/etc//ssh/ssh_host_ed25519_key") == "denied_path"
def test_noncanonical_backup_and_log_fastpaths():
pol = IgnorePolicy()
assert pol._path_deny_reason("/var/log/foo/../bar.log") == "log_file"
assert pol._path_deny_reason("/etc/foo/../something~") == "backup_file"
assert pol._path_deny_reason("/etc//passwd-") == "backup_file"
def test_inspect_file_refuses_symlinked_parent_directory(tmp_path: Path):
"""A regular file reached through a symlinked *parent* directory must be
refused, even though O_NOFOLLOW alone would only guard the final
component. Otherwise a file whose real location is deny-globbed (or whose
content is benign) could be captured while its logical path looks safe.
"""
pol = IgnorePolicy()
secret = tmp_path / "secretroot"
secret.mkdir()
(secret / "config").write_text("listen_port=8080\n", encoding="utf-8")
(tmp_path / "allowed").symlink_to(secret)
reason, inspection = pol.inspect_file(str(tmp_path / "allowed" / "config"))
assert reason == "symlink_component"
assert inspection is None
def test_inspect_file_refuses_denyglob_evasion_via_symlinked_parent(tmp_path: Path):
"""The strongest variant: the real file lives under a deny-globbed dir,
but is reached via a symlinked parent so the *logical* path does not match
the deny glob. Content is non-secret-looking (DH params), so only the
parent-symlink check stands between the operator and disclosure.
"""
pol = IgnorePolicy()
realdir = tmp_path / "ssl_private"
realdir.mkdir()
(realdir / "dhparam.pem").write_text(
"-----BEGIN DH PARAMETERS-----\nMII...\n-----END DH PARAMETERS-----\n",
encoding="utf-8",
)
(tmp_path / "innocent").symlink_to(realdir)
reason, inspection = pol.inspect_file(str(tmp_path / "innocent" / "dhparam.pem"))
assert reason == "symlink_component"
assert inspection is None
def test_inspect_file_still_captures_normal_nested_file(tmp_path: Path):
"""Regression guard: ordinary files in real (non-symlinked) directories
must still be inspected and returned.
"""
pol = IgnorePolicy()
nested = tmp_path / "etc" / "myapp"
nested.mkdir(parents=True)
(nested / "app.conf").write_text("workers=4\n", encoding="utf-8")
reason, inspection = pol.inspect_file(str(nested / "app.conf"))
assert reason is None
assert inspection is not None
assert inspection.data == b"workers=4\n"