diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d4e73d9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + + - package-ecosystem: uv + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3f374e..1698565 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,15 +18,32 @@ jobs: - name: Test run: rite test + # One job per linter so the PR checks list shows each check's status + # at a glance instead of bundling them behind a single "lint" line. lint: + strategy: + fail-fast: false + matrix: + check: [pylint, ruff, shell, actions] + name: lint:${{ matrix.check }} runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: jdx/mise-action@v2 - - name: Lint - run: rite lint + - name: Lint (${{ matrix.check }}) + run: rite lint:${{ matrix.check }} + + audit: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: jdx/mise-action@v2 + + - name: Audit dependencies for known vulnerabilities + run: rite audit # "Does it build": render the formula against a tarball of HEAD, brew # install it, run the formula test block, then a real `macprefs backup` diff --git a/Ritefile.yml b/Ritefile.yml index afdf130..63b9dfd 100644 --- a/Ritefile.yml +++ b/Ritefile.yml @@ -28,11 +28,11 @@ tasks: - uv sync test: - desc: Run tests with coverage + desc: Run tests with coverage (fails under 95%) aliases: [t] deps: [setup] cmds: - - uv run pytest --cov=src --cov-report xml:cov.xml --cov-report term-missing + - uv run pytest --cov=src --cov-fail-under=95 --cov-report xml:cov.xml --cov-report term-missing test:watch: desc: Re-run tests on file changes @@ -42,14 +42,50 @@ tasks: - uv run ptw --nobeep lint: - desc: Run pylint + desc: Run all linters in parallel (pylint, ruff, shellcheck, actionlint) aliases: [l] + deps: [lint:pylint, lint:ruff, lint:shell, lint:actions] + + lint:pylint: + desc: Run pylint deps: [setup] cmds: # src/macprefs is listed explicitly: it has no .py extension, so the # glob misses it, but pylint lints any file passed by name. - uv run pylint src/*.py src/macprefs tests/*.py + lint:ruff: + desc: Run ruff check + format check + deps: [setup] + cmds: + - uv run ruff check src tests + - uv run ruff format --check src tests + + lint:shell: + desc: Shellcheck the ci scripts + cmds: + - shellcheck ci/scripts/*.sh + + lint:actions: + desc: Lint the GitHub Actions workflows + cmds: + - actionlint + + format: + desc: Format the codebase with ruff (also sorts imports) + aliases: [f] + deps: [setup] + cmds: + - uv run ruff check --fix src tests + - uv run ruff format src tests + + audit: + desc: Scan locked dependencies for known vulnerabilities + aliases: [a] + deps: [setup] + cmds: + - uv run pip-audit --skip-editable + build: desc: Build the brew package from committed HEAD, install, verify, uninstall aliases: [b] diff --git a/ci/scripts/brew-build-test.sh b/ci/scripts/brew-build-test.sh index e09a1de..6f02fdc 100755 --- a/ci/scripts/brew-build-test.sh +++ b/ci/scripts/brew-build-test.sh @@ -32,6 +32,15 @@ brew tap-new --no-git "$TAP" >/dev/null trap 'brew uninstall --force macprefs >/dev/null 2>&1 || true; brew untap -f "$TAP" >/dev/null 2>&1 || true; rm -rf "$TMP"' EXIT cp "$TMP/macprefs.rb" "$(brew --repository "$TAP")/Formula/macprefs.rb" +# Style/audit the rendered formula before installing — catches formula-API +# deprecations (like the old `depends_on :python3`) before Homebrew breaks +# the tap. --except checks that can't pass for a local file:// url render. +brew style "$TAP/macprefs" +brew audit --strict --except=url,stable_version,github_repository "$TAP/macprefs" || { + echo "brew audit failed" >&2 + exit 1 +} + brew install "$TAP/macprefs" # Formula's own test block (runs `macprefs --help`). diff --git a/macprefs.template.rb b/macprefs.template.rb index 5a2a08c..8139f69 100644 --- a/macprefs.template.rb +++ b/macprefs.template.rb @@ -12,6 +12,6 @@ def install end test do - system "#{bin}/macprefs", "--help" + system bin/"macprefs", "--help" end end diff --git a/mise.toml b/mise.toml index 960ea71..e2f7665 100644 --- a/mise.toml +++ b/mise.toml @@ -2,3 +2,5 @@ python = "3.13" uv = "latest" "github:clintmod/rite" = "latest" +shellcheck = "latest" +actionlint = "latest" diff --git a/pyproject.toml b/pyproject.toml index 20fefbc..37f3fa2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,11 +12,13 @@ license = { file = "LICENSE" } [dependency-groups] dev = [ + "pip-audit==2.10.1", "pylint==4.0.6", "pytest==9.1.1", "pytest-cov==7.1.0", "pytest-testmon==2.2.0", "pytest-watch==4.2.0", + "ruff==0.15.22", ] [tool.uv] @@ -70,3 +72,13 @@ score = false [tool.pylint.format] max-line-length = 130 + +[tool.ruff] +line-length = 130 +target-version = "py310" +# The extensionless entrypoint needs to be named to be included. +include = ["src/**/*.py", "tests/**/*.py", "src/macprefs"] + +[tool.ruff.lint] +# Default rules (pyflakes + core pycodestyle) plus import sorting. +extend-select = ["I"] diff --git a/src/app_store_preferences.py b/src/app_store_preferences.py index 8cadd3f..e47ae65 100644 --- a/src/app_store_preferences.py +++ b/src/app_store_preferences.py @@ -1,23 +1,24 @@ -from os import listdir, path import logging as log -from config import get_app_store_preferences_backup_dir, get_app_store_preferences_dir, ensure_exists +from os import listdir, path + +from config import ensure_exists, get_app_store_preferences_backup_dir, get_app_store_preferences_dir from utils import copy_file, copy_files, execute_shell, restart_cfprefsd def backup(): - log.info('Backing up app store preferences (.plist)...') + log.info("Backing up app store preferences (.plist)...") files = build_file_list() dest = get_app_store_preferences_backup_dir() copy_files(files, dest) def restore(): - log.info('Restoring app store preferences (.plist)...') + log.info("Restoring app store preferences (.plist)...") source = get_app_store_preferences_backup_dir() dest = get_app_store_preferences_dir() for f in listdir(source): - domain = f.split('.plist')[0] - dest_path = path.join(dest, domain, 'Data/Library/Preferences') + domain = f.split(".plist")[0] + dest_path = path.join(dest, domain, "Data/Library/Preferences") ensure_exists(dest_path) source_file = path.join(source, f) copy_file(source_file, dest_path) @@ -26,7 +27,7 @@ def restore(): def build_file_list(): source = get_app_store_preferences_dir() - command = 'find ' + source + '*/Data/Library/Preferences -type f -name "*.plist" 2>/dev/null' + command = "find " + source + '*/Data/Library/Preferences -type f -name "*.plist" 2>/dev/null' result = execute_shell(command, is_shell=True, suppress_errors=True) - files = result.strip().split('\n') + files = result.strip().split("\n") return files diff --git a/src/config.py b/src/config.py index e2c1e3e..4eb53c4 100644 --- a/src/config.py +++ b/src/config.py @@ -1,56 +1,56 @@ -from os import environ, makedirs, path, getenv import getpass +from os import environ, getenv, makedirs, path def get_macprefs_dir(): - backup_dir = '' - if 'MACPREFS_BACKUP_DIR' in environ: - backup_dir = environ['MACPREFS_BACKUP_DIR'] + backup_dir = "" + if "MACPREFS_BACKUP_DIR" in environ: + backup_dir = environ["MACPREFS_BACKUP_DIR"] else: - backup_dir = path.join(get_home_dir(), 'Dropbox', 'MacPrefsBackup') + backup_dir = path.join(get_home_dir(), "Dropbox", "MacPrefsBackup") ensure_exists(backup_dir) return backup_dir def get_preferences_dir(): - return_val = path.join(get_home_dir(), 'Library/Preferences/') + return_val = path.join(get_home_dir(), "Library/Preferences/") return return_val def get_preferences_backup_dir(): - return_val = path.join(get_macprefs_dir(), 'preferences/') + return_val = path.join(get_macprefs_dir(), "preferences/") ensure_exists(return_val) return return_val def get_sys_preferences_backup_dir(): - return_val = path.join(get_macprefs_dir(), 'system_preferences/') + return_val = path.join(get_macprefs_dir(), "system_preferences/") ensure_exists(return_val) return return_val def get_shared_file_lists_backup_dir(): - return_val = path.join(get_macprefs_dir(), 'shared_file_lists/') + return_val = path.join(get_macprefs_dir(), "shared_file_lists/") ensure_exists(return_val) return return_val def get_shared_file_lists_dir(): - return path.join(get_home_dir(), 'Library/Application Support/com.apple.sharedfilelist/') + return path.join(get_home_dir(), "Library/Application Support/com.apple.sharedfilelist/") def get_dotfiles_backup_dir(): - return_val = path.join(get_macprefs_dir(), 'dotfiles/') + return_val = path.join(get_macprefs_dir(), "dotfiles/") ensure_exists(return_val) return return_val def get_dotfile_excludes(): - return ['.CFUserTextEncoding', '.DS_Store'] + return [".CFUserTextEncoding", ".DS_Store"] def get_home_dir(): - return getenv('HOME') + '/' + return getenv("HOME") + "/" def ensure_exists(input_dir): @@ -59,13 +59,13 @@ def ensure_exists(input_dir): def get_ssh_backup_dir(): - return_val = path.join(get_macprefs_dir(), 'ssh/') + return_val = path.join(get_macprefs_dir(), "ssh/") ensure_exists(return_val) return return_val def get_ssh_user_dir(): - return_val = path.join(get_home_dir(), '.ssh/') + return_val = path.join(get_home_dir(), ".ssh/") return return_val @@ -74,55 +74,51 @@ def get_user(): def get_internet_accounts_dir(): - return path.join(get_home_dir(), 'Library/Accounts/') + return path.join(get_home_dir(), "Library/Accounts/") def get_internet_accounts_backup_dir(): - return_val = path.join( - get_macprefs_dir(), 'Accounts/') + return_val = path.join(get_macprefs_dir(), "Accounts/") ensure_exists(return_val) return return_val def get_user_launch_agents_dir(): - return path.join(get_home_dir(), 'Library/LaunchAgents/') + return path.join(get_home_dir(), "Library/LaunchAgents/") def get_user_launch_agents_backup_dir(): - return_val = path.join( - get_macprefs_dir(), 'StartupItems/LaunchAgents/User/') + return_val = path.join(get_macprefs_dir(), "StartupItems/LaunchAgents/User/") ensure_exists(return_val) return return_val def get_system_launch_agents_dir(): - return '/Library/LaunchAgents/' + return "/Library/LaunchAgents/" def get_system_launch_agents_backup_dir(): - return_val = path.join( - get_macprefs_dir(), 'StartupItems/LaunchAgents/AllUsers/') + return_val = path.join(get_macprefs_dir(), "StartupItems/LaunchAgents/AllUsers/") ensure_exists(return_val) return return_val def get_system_launch_daemons_dir(): - return '/Library/LaunchDaemons/' + return "/Library/LaunchDaemons/" def get_system_launch_daemons_backup_dir(): - return_val = path.join( - get_macprefs_dir(), 'StartupItems/LaunchDaemons/AllUsers/') + return_val = path.join(get_macprefs_dir(), "StartupItems/LaunchDaemons/AllUsers/") ensure_exists(return_val) return return_val def get_app_store_preferences_dir(): - return_val = path.join(get_home_dir(), 'Library/Containers/') + return_val = path.join(get_home_dir(), "Library/Containers/") return return_val def get_app_store_preferences_backup_dir(): - return_val = path.join(get_macprefs_dir(), 'app_store_preferences/') + return_val = path.join(get_macprefs_dir(), "app_store_preferences/") ensure_exists(return_val) return return_val diff --git a/src/dotfiles.py b/src/dotfiles.py index ce48346..d1567e4 100644 --- a/src/dotfiles.py +++ b/src/dotfiles.py @@ -1,11 +1,12 @@ -from os import path, listdir import logging as log -from config import get_dotfiles_backup_dir, get_dotfile_excludes, get_home_dir, get_user +from os import listdir, path + +from config import get_dotfile_excludes, get_dotfiles_backup_dir, get_home_dir, get_user from utils import copy_files, ensure_files_owned_by_user def backup(): - log.info('Backing up dotfiles...') + log.info("Backing up dotfiles...") # build file list home_dir = get_home_dir() excludes = get_dotfile_excludes() @@ -15,7 +16,7 @@ def backup(): def restore(): - log.info('Restoring dotfiles...') + log.info("Restoring dotfiles...") source = get_dotfiles_backup_dir() dest = get_home_dir() files = get_dot_files(source) @@ -30,6 +31,6 @@ def get_dot_files(home_dir, excludes=None): files = [] for f in listdir(home_dir): full_file_path = path.join(home_dir, f) - if f[0] == '.' and path.isfile(full_file_path) and f not in excludes: + if f[0] == "." and path.isfile(full_file_path) and f not in excludes: files.append(full_file_path) return files diff --git a/src/internet_accounts.py b/src/internet_accounts.py index 3355c35..721b2a7 100644 --- a/src/internet_accounts.py +++ b/src/internet_accounts.py @@ -1,15 +1,16 @@ import logging as log -from utils import copy_dir, ensure_dir_owned_by_user + import config +from utils import copy_dir, ensure_dir_owned_by_user def backup(): - log.info('Backing up internet accounts db files...') + log.info("Backing up internet accounts db files...") backup_internet_accounts() def restore(): - log.info('Restoring internet accounts db files...') + log.info("Restoring internet accounts db files...") restore_internet_accounts() diff --git a/src/macprefs b/src/macprefs index 65be02e..13e6a72 100755 --- a/src/macprefs +++ b/src/macprefs @@ -1,74 +1,76 @@ #!/usr/bin/env python3 import argparse -import sys import logging as log +import sys + +import app_store_preferences import config -from version import __version__ import dotfiles +import internet_accounts import preferences import shared_file_lists -import system_preferences import ssh_files import startup_items -import app_store_preferences -import internet_accounts +import system_preferences +from version import __version__ preference_choices = [ - 'system_preferences', - 'startup_items', - 'dotfiles', - 'shared_file_lists', - 'ssh_files', - 'preferences', - 'app_store_preferences', - 'internet_accounts' + "system_preferences", + "startup_items", + "dotfiles", + "shared_file_lists", + "ssh_files", + "preferences", + "app_store_preferences", + "internet_accounts", ] + def backup(choices=[]): - if not choices or 'system_preferences' in choices: + if not choices or "system_preferences" in choices: system_preferences.backup() - if not choices or 'startup_items' in choices: + if not choices or "startup_items" in choices: startup_items.backup() - if not choices or 'dotfiles' in choices: + if not choices or "dotfiles" in choices: dotfiles.backup() - if not choices or 'shared_file_lists' in choices: + if not choices or "shared_file_lists" in choices: shared_file_lists.backup() - if not choices or 'ssh_files' in choices: + if not choices or "ssh_files" in choices: ssh_files.backup() - if not choices or 'preferences' in choices: + if not choices or "preferences" in choices: preferences.backup() - if not choices or 'app_store_preferences' in choices: + if not choices or "app_store_preferences" in choices: app_store_preferences.backup() - if not choices or 'internet_accounts' in choices: + if not choices or "internet_accounts" in choices: internet_accounts.backup() - print('Backup Complete.') + print("Backup Complete.") def restore(choices=[]): - if not choices or 'system_preferences' in choices: + if not choices or "system_preferences" in choices: system_preferences.restore() - if not choices or 'startup_items' in choices: + if not choices or "startup_items" in choices: startup_items.restore() - if not choices or 'dotfiles' in choices: + if not choices or "dotfiles" in choices: dotfiles.restore() - if not choices or 'shared_file_lists' in choices: + if not choices or "shared_file_lists" in choices: shared_file_lists.restore() - if not choices or 'ssh_files' in choices: + if not choices or "ssh_files" in choices: ssh_files.restore() - if not choices or 'preferences' in choices: + if not choices or "preferences" in choices: preferences.restore() - if not choices or 'app_store_preferences' in choices: + if not choices or "app_store_preferences" in choices: app_store_preferences.restore() - if not choices or 'internet_accounts' in choices: + if not choices or "internet_accounts" in choices: internet_accounts.restore() - print('Restore Complete.') + print("Restore Complete.") def invoke_func(args): if args.func is not None: - if args.name in ('backup', 'restore'): + if args.name in ("backup", "restore"): args.func(args.t) else: args.func() @@ -77,29 +79,30 @@ def invoke_func(args): def configure_logging(verbose): if verbose > 0: log.basicConfig(format="%(message)s", level=log.DEBUG) - log.debug('Verbose logging enabled') + log.debug("Verbose logging enabled") else: log.basicConfig(format="%(message)s", level=log.INFO) def main(): backup_dir = config.get_macprefs_dir() - parser = argparse.ArgumentParser( - prog='macprefs', description='backup and restore mac system preferences') - parser.add_argument('--version', action='version', version=__version__) - parser.add_argument('--verbose', '-v', action='count', help='log everything to the console') - - subparsers = parser.add_subparsers(title='commands', metavar='') - - backup_parser = subparsers.add_parser( - 'backup', help='backup preferences to ' + backup_dir) - backup_parser.set_defaults(name='backup', func=backup) - backup_parser.add_argument('-t', nargs='*', metavar='type', help='preferences you want to backup', choices=preference_choices, action="extend") - - restore_parser = subparsers.add_parser( - 'restore', help='restore preferences from ' + backup_dir) - restore_parser.set_defaults(name='restore', func=restore) - restore_parser.add_argument('-t', nargs='*', metavar='type', help='preferences you want to restore', choices=preference_choices, action="extend") + parser = argparse.ArgumentParser(prog="macprefs", description="backup and restore mac system preferences") + parser.add_argument("--version", action="version", version=__version__) + parser.add_argument("--verbose", "-v", action="count", help="log everything to the console") + + subparsers = parser.add_subparsers(title="commands", metavar="") + + backup_parser = subparsers.add_parser("backup", help="backup preferences to " + backup_dir) + backup_parser.set_defaults(name="backup", func=backup) + backup_parser.add_argument( + "-t", nargs="*", metavar="type", help="preferences you want to backup", choices=preference_choices, action="extend" + ) + + restore_parser = subparsers.add_parser("restore", help="restore preferences from " + backup_dir) + restore_parser.set_defaults(name="restore", func=restore) + restore_parser.add_argument( + "-t", nargs="*", metavar="type", help="preferences you want to restore", choices=preference_choices, action="extend" + ) if len(sys.argv) == 1: parser.print_help() @@ -110,5 +113,5 @@ def main(): invoke_func(args) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/preferences.py b/src/preferences.py index 3adc92f..0878b14 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -1,17 +1,18 @@ import logging as log + from config import get_preferences_backup_dir, get_preferences_dir, get_user from utils import copy_dir, ensure_dir_owned_by_user, restart_cfprefsd def backup(): - log.info('Backing up preferences (.plist)...') + log.info("Backing up preferences (.plist)...") source = get_preferences_dir() dest = get_preferences_backup_dir() copy_dir(source, dest) def restore(): - log.info('Restoring preferences (.plist)...') + log.info("Restoring preferences (.plist)...") source = get_preferences_backup_dir() dest = get_preferences_dir() copy_dir(source, dest, with_sudo=True) diff --git a/src/publish.py b/src/publish.py index 65bac63..a67c0cd 100644 --- a/src/publish.py +++ b/src/publish.py @@ -1,128 +1,130 @@ -import sys +import base64 import glob -import os import json -import urllib.request +import os +import sys import urllib.error import urllib.parse -import base64 +import urllib.request + from utils import execute_shell, is_none_or_empty_string from version import __version__ def check_for_uncommitted_files(): - print('Checking for uncommitted files...') - result = execute_shell(['git', 'status']) - if not 'nothing to commit' in result: - raise ValueError( - 'There are uncommitted files in the workspace. Commit or stash them before trying to publish.') + print("Checking for uncommitted files...") + result = execute_shell(["git", "status"]) + if "nothing to commit" not in result: + raise ValueError("There are uncommitted files in the workspace. Commit or stash them before trying to publish.") def create_version_tag_and_push(tag): - print('Tagging git repository with version ' + tag) - execute_shell(['git', 'tag', tag]) - print('Pushing the new tag to github...') - execute_shell(['git', 'push', 'origin', 'HEAD', '--tags']) + print("Tagging git repository with version " + tag) + execute_shell(["git", "tag", tag]) + print("Pushing the new tag to github...") + execute_shell(["git", "push", "origin", "HEAD", "--tags"]) def download_tar(filename): - print('Downloading the new version...') - urllib.request.urlretrieve( - 'https://github.com/clintmod/macprefs/archive/' + filename, filename) + print("Downloading the new version...") + urllib.request.urlretrieve("https://github.com/clintmod/macprefs/archive/" + filename, filename) def calc_sha256(filename): - print('Calculating the sha256 of the tarball...') - result = execute_shell(['shasum', '-a', '256', filename]) + print("Calculating the sha256 of the tarball...") + result = execute_shell(["shasum", "-a", "256", filename]) print(result) - sha256 = result.split(' ')[0] + sha256 = result.split(" ")[0] print(sha256) return sha256 def create_brew_formula_file_content(version, sha256): - print('Generating base64 encoded brew formula...') + print("Generating base64 encoded brew formula...") # Read in the file - with open('macprefs.template.rb', 'r') as f: + with open("macprefs.template.rb", "r") as f: filedata = f.read() # Replace - filedata = filedata.replace('###sha256###', sha256) - filedata = filedata.replace('###version###', version) - filedata_bytes = bytes(filedata, 'utf-8') - filedata = base64.b64encode(filedata_bytes).decode('utf-8') + filedata = filedata.replace("###sha256###", sha256) + filedata = filedata.replace("###version###", version) + filedata_bytes = bytes(filedata, "utf-8") + filedata = base64.b64encode(filedata_bytes).decode("utf-8") return filedata def get_sha_of_old_macprefs_formula(): - print('Getting sha of old macprefs formula from github...') - result = json.load(urllib.request.urlopen( - 'https://api.github.com/repos/clintmod/homebrew-formulas/contents/Formula/macprefs.rb')) + print("Getting sha of old macprefs formula from github...") + result = json.load( + urllib.request.urlopen("https://api.github.com/repos/clintmod/homebrew-formulas/contents/Formula/macprefs.rb") + ) # print 'sha = ' + result['sha'] - return result['sha'] + return result["sha"] def upload_new_brew_formula(content, version, sha): - print('Uploading the new macprefs formula to https://github.com/clintmod/homebrew-formulas') - token = os.environ['MACPREFS_TOKEN'] - auth_header = 'Authorization: token ' + token - json_header = 'Content-Type: application/json' - data = json.dumps({ - 'path': 'Formula/macprefs.rb', - 'message': 'Updating to version ' + version, - 'committer': {'name': 'Clint M', 'email': 'cmodien@gmail.com'}, - 'content': content, - 'branch': 'master', - 'sha': sha, - }) - with open('github_request.json', 'w') as f: + print("Uploading the new macprefs formula to https://github.com/clintmod/homebrew-formulas") + token = os.environ["MACPREFS_TOKEN"] + auth_header = "Authorization: token " + token + json_header = "Content-Type: application/json" + data = json.dumps( + { + "path": "Formula/macprefs.rb", + "message": "Updating to version " + version, + "committer": {"name": "Clint M", "email": "cmodien@gmail.com"}, + "content": content, + "branch": "master", + "sha": sha, + } + ) + with open("github_request.json", "w") as f: f.write(data) commands = [ - 'curl', - '-i', - '-X', - 'PUT', - '-H', + "curl", + "-i", + "-X", + "PUT", + "-H", auth_header, - '-H', + "-H", json_header, - '-d', - '@github_request.json', - 'https://api.github.com/repos/clintmod/homebrew-formulas/contents/Formula/macprefs.rb' + "-d", + "@github_request.json", + "https://api.github.com/repos/clintmod/homebrew-formulas/contents/Formula/macprefs.rb", ] result = execute_shell(commands) - if 'Status: 200 OK' not in result: - raise ValueError('Error uploading new brew formula to github - result\n', result) + if "Status: 200 OK" not in result: + raise ValueError("Error uploading new brew formula to github - result\n", result) return data def cleanup(): - print('Cleaning up...') - for f in glob.glob('*.tar.gz'): + print("Cleaning up...") + for f in glob.glob("*.tar.gz"): os.remove(f) - os.remove('github_request.json') + os.remove("github_request.json") def download_macprefs(): - print('Running brew update macprefs to verify version...') - result = execute_shell(['brew', 'upgrade', 'macprefs'], False, '.', True) + print("Running brew update macprefs to verify version...") + result = execute_shell(["brew", "upgrade", "macprefs"], False, ".", True) if not is_none_or_empty_string(result): print(result) def verify_macprefs(): - result = execute_shell(['macprefs', '--version']) - message = '\nworkspace:\t' + __version__ + '\ninstalled:\t' + result + result = execute_shell(["macprefs", "--version"]) + message = "\nworkspace:\t" + __version__ + "\ninstalled:\t" + result assert __version__ in result, message - print('version check verified' + message) + print("version check verified" + message) def main(): - if len(sys.argv) > 1 and sys.argv[1] == '-test': + if len(sys.argv) > 1 and sys.argv[1] == "-test": return False version = __version__ check_for_uncommitted_files() create_version_tag_and_push(version) - filename = version + '.tar.gz' + filename = version + ".tar.gz" download_tar(filename) sha256 = calc_sha256(filename) content = create_brew_formula_file_content(version, sha256) @@ -132,10 +134,10 @@ def main(): download_macprefs() verify_macprefs() - print('Success') + print("Success") return True -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/shared_file_lists.py b/src/shared_file_lists.py index 560a840..cd593f5 100644 --- a/src/shared_file_lists.py +++ b/src/shared_file_lists.py @@ -1,22 +1,23 @@ -from os.path import exists import logging as log -from utils import copy_dir, ensure_dir_owned_by_user +from os.path import exists + import config +from utils import copy_dir, ensure_dir_owned_by_user def backup(): - log.info('Backing up shared file lists...') + log.info("Backing up shared file lists...") source = config.get_shared_file_lists_dir() if not exists(source): - log.info('Warning: %s does not exist.', source) - log.info('Shared file backup failed.') + log.info("Warning: %s does not exist.", source) + log.info("Shared file backup failed.") return dest = config.get_shared_file_lists_backup_dir() copy_dir(source, dest) def restore(): - log.info('Restoring shared file lists...') + log.info("Restoring shared file lists...") dest = config.get_shared_file_lists_dir() source = config.get_shared_file_lists_backup_dir() copy_dir(source, dest, with_sudo=True) diff --git a/src/ssh_files.py b/src/ssh_files.py index 70d661b..86cdc58 100644 --- a/src/ssh_files.py +++ b/src/ssh_files.py @@ -1,15 +1,16 @@ -from os.path import exists import logging as log -from config import get_ssh_backup_dir, get_ssh_user_dir, get_user, ensure_exists +from os.path import exists + +from config import ensure_exists, get_ssh_backup_dir, get_ssh_user_dir, get_user from utils import copy_dir, ensure_dir_owned_by_user def backup(): source = get_ssh_user_dir() if not exists(source): - log.info('No .ssh dir found... skipping.') + log.info("No .ssh dir found... skipping.") return - log.info('Backing up .ssh dir...') + log.info("Backing up .ssh dir...") dest = get_ssh_backup_dir() ensure_exists(dest) copy_dir(source, dest) @@ -18,11 +19,9 @@ def backup(): def restore(): source = get_ssh_backup_dir() if not exists(source): - log.info('No .ssh dir found... skipping.') + log.info("No .ssh dir found... skipping.") return - log.info('Restoring .ssh dir...') + log.info("Restoring .ssh dir...") dest = get_ssh_user_dir() - copy_dir( - source, dest, with_sudo=True - ) - ensure_dir_owned_by_user(dest, get_user()) \ No newline at end of file + copy_dir(source, dest, with_sudo=True) + ensure_dir_owned_by_user(dest, get_user()) diff --git a/src/startup_items.py b/src/startup_items.py index 1cb6000..e0703e9 100644 --- a/src/startup_items.py +++ b/src/startup_items.py @@ -1,17 +1,18 @@ import logging as log -from utils import copy_dir, ensure_dir_owned_by_user + import config +from utils import copy_dir, ensure_dir_owned_by_user def backup(): - log.info('Backing up start up items...') + log.info("Backing up start up items...") backup_user_launch_agents() backup_system_launch_agents() backup_system_daemons_agents() def restore(): - log.info('Restoring start up items...') + log.info("Restoring start up items...") restore_user_launch_agents() restore_system_launch_agents() restore_system_daemons_agents() @@ -46,11 +47,11 @@ def restore_system_launch_agents(): source = config.get_system_launch_agents_backup_dir() dest = config.get_system_launch_agents_dir() copy_dir(source, dest, with_sudo=True) - ensure_dir_owned_by_user(dest, 'root:wheel', '644') + ensure_dir_owned_by_user(dest, "root:wheel", "644") def restore_system_daemons_agents(): source = config.get_system_launch_daemons_backup_dir() dest = config.get_system_launch_daemons_dir() copy_dir(source, dest, with_sudo=True) - ensure_dir_owned_by_user(dest, 'root:wheel', '644') + ensure_dir_owned_by_user(dest, "root:wheel", "644") diff --git a/src/system_preferences.py b/src/system_preferences.py index 85c287a..4edfb8f 100644 --- a/src/system_preferences.py +++ b/src/system_preferences.py @@ -1,11 +1,12 @@ -from os import path import logging as log -from utils import copy_dir, ensure_files_owned_by_user, restart_cfprefsd +from os import path + from config import get_sys_preferences_backup_dir +from utils import copy_dir, ensure_files_owned_by_user, restart_cfprefsd def backup(): - log.info('Backing up system preferences... ') + log.info("Backing up system preferences... ") source = get_pm_path() dest = get_pm_backup_path() copy_dir(source, dest) @@ -14,20 +15,21 @@ def backup(): def restore(): source = get_pm_backup_path() dest = get_pm_path() - log.info('Restoring system preferences...') + log.info("Restoring system preferences...") copy_dir(source, dest, with_sudo=True) - ensure_files_owned_by_user('root:wheel', [dest], '644') + ensure_files_owned_by_user("root:wheel", [dest], "644") restart_cfprefsd() -pm_file_name = 'com.apple.PowerManagement.plist' +pm_file_name = "com.apple.PowerManagement.plist" + def get_pm_backup_path(): return path.join(get_sys_preferences_backup_dir(), pm_file_name) def get_pm_path(): - pm_path = path.join('/Library/Preferences/', pm_file_name) + pm_path = path.join("/Library/Preferences/", pm_file_name) if not path.exists(pm_path): - pm_path = path.join('/Library/Preferences/SystemConfiguration/', pm_file_name) + pm_path = path.join("/Library/Preferences/SystemConfiguration/", pm_file_name) return pm_path diff --git a/src/utils.py b/src/utils.py index ad69113..5d6165d 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,29 +1,27 @@ -from subprocess import CalledProcessError, check_output, STDOUT -import sys import importlib import logging as log +import sys +from subprocess import STDOUT, CalledProcessError, check_output -def execute_shell(command, is_shell=False, cwd='.', suppress_errors=False): - output = '' - log.debug('\n--- executing shell command ----\n') - log.debug('setting working dir to: %s', cwd) - log.debug('command: %s', str(command)) +def execute_shell(command, is_shell=False, cwd=".", suppress_errors=False): + output = "" + log.debug("\n--- executing shell command ----\n") + log.debug("setting working dir to: %s", cwd) + log.debug("command: %s", str(command)) try: - output = check_output(command, shell=is_shell, - cwd=cwd, stderr=STDOUT).strip().decode('utf-8') - log.debug('output = %s', output) + output = check_output(command, shell=is_shell, cwd=cwd, stderr=STDOUT).strip().decode("utf-8") + log.debug("output = %s", output) except CalledProcessError as err: - log.error('Error Info:\nerror code = %s\ncmd %s\nerror message:%s', - err.returncode, err.cmd, err.output) + log.error("Error Info:\nerror code = %s\ncmd %s\nerror message:%s", err.returncode, err.cmd, err.output) output = err.output # when the check_output raises an error, it also stores the result as bytes. if isinstance(output, bytes): - output = output.decode('ascii') + output = output.decode("ascii") if not suppress_errors: raise finally: - log.debug('\n---- shell execution finished ---\n') + log.debug("\n---- shell execution finished ---\n") return output @@ -32,7 +30,7 @@ def restart_cfprefsd(): # .plist files with its stale cache. Killing it forces launchd to relaunch # it on demand so restored preferences are picked up. killall exits nonzero # when no process matches, which is harmless here, so suppress errors. - execute_shell(['sudo', 'killall', 'cfprefsd'], suppress_errors=True) + execute_shell(["sudo", "killall", "cfprefsd"], suppress_errors=True) def run_rsync(command): @@ -44,8 +42,7 @@ def run_rsync(command): execute_shell(command) except CalledProcessError as err: if err.returncode in (23, 24): - log.warning('rsync partial transfer (exit %s): %s', - err.returncode, err.output) + log.warning("rsync partial transfer (exit %s): %s", err.returncode, err.output) else: raise @@ -53,36 +50,36 @@ def run_rsync(command): def copy_dir(src, dest, with_sudo=False): extra_args = [] if log.root.getEffectiveLevel() == log.DEBUG: - extra_args = ['-vv'] - command = ['rsync', '-a'] + extra_args + [src, dest] + extra_args = ["-vv"] + command = ["rsync", "-a"] + extra_args + [src, dest] if with_sudo: - command = ['sudo'] + command + command = ["sudo"] + command run_rsync(command) def copy_files(files, dest): extra_args = [] if log.root.getEffectiveLevel() == log.DEBUG: - extra_args = ['-vv'] - command = ['rsync', '-a'] + extra_args + files + [dest] + extra_args = ["-vv"] + command = ["rsync", "-a"] + extra_args + files + [dest] run_rsync(command) def copy_file(fle, dest): extra_args = [] if log.root.getEffectiveLevel() == log.DEBUG: - extra_args = ['-vv'] - command = ['rsync', '-a'] + extra_args + [fle, dest] + extra_args = ["-vv"] + command = ["rsync", "-a"] + extra_args + [fle, dest] run_rsync(command) -def ensure_dir_owned_by_user(path, user, mode='600'): +def ensure_dir_owned_by_user(path, user, mode="600"): change_mode(path, mode) change_owner(path, user) ensure_subdirs_listable(path) -def ensure_files_owned_by_user(user, files, mode='600'): +def ensure_files_owned_by_user(user, files, mode="600"): change_mode_for_files(files, mode) change_owner_for_files(files, user) @@ -93,42 +90,42 @@ def run_permission_change(command): # suppress the error and warn -- restoring everything else still succeeds. result = execute_shell(command, suppress_errors=True) if not is_none_or_empty_string(result): - log.warning('permission change failed: %s', result) + log.warning("permission change failed: %s", result) def change_owner_for_files(files, user): - command = ['sudo', 'chown', user] + files + command = ["sudo", "chown", user] + files run_permission_change(command) def change_mode_for_files(files, mode): - command = ['sudo', 'chmod', str(mode)] + files + command = ["sudo", "chmod", str(mode)] + files run_permission_change(command) def change_owner(path, owner, should_recurse=True): - command = ['sudo', 'chown'] + command = ["sudo", "chown"] if should_recurse: - command += ['-R'] + command += ["-R"] command += [owner, path] run_permission_change(command) def change_mode(path, mode, should_recurse=True): - command = ['sudo', 'chmod'] + command = ["sudo", "chmod"] if should_recurse: - command += ['-R'] + command += ["-R"] command += [str(mode), path] run_permission_change(command) def ensure_subdirs_listable(path): - command = ['sudo', 'chmod', '-R', 'a+X', path] + command = ["sudo", "chmod", "-R", "a+X", path] run_permission_change(command) def is_none_or_empty_string(val): - if val is None or val == '': + if val is None or val == "": return True return False diff --git a/src/version.py b/src/version.py index 08d27a8..3359967 100644 --- a/src/version.py +++ b/src/version.py @@ -1 +1 @@ -__version__ = 'v1.0.27' +__version__ = "v1.0.27" diff --git a/tests/test_app_store_preferences.py b/tests/test_app_store_preferences.py index 2a81c24..e4fb3bc 100644 --- a/tests/test_app_store_preferences.py +++ b/tests/test_app_store_preferences.py @@ -1,4 +1,5 @@ from unittest.mock import patch + import app_store_preferences @@ -7,6 +8,7 @@ def test_backup(copy_mock): app_store_preferences.backup() assert copy_mock.call_count > 0 + @patch("app_store_preferences.restart_cfprefsd") @patch("app_store_preferences.copy_file") def test_restore(copy_mock, restart_mock): diff --git a/tests/test_config.py b/tests/test_config.py index c627f2f..95b9b52 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,22 +4,23 @@ import config + def test_get_macprefs_dir(): backup_dir = config.get_macprefs_dir() assert backup_dir is not None -@patch('config.makedirs') +@patch("config.makedirs") # pylint: disable=unused-argument def test_get_macprefs_dir_works_with_environ(makedirs_mock): - os.environ['MACPREFS_BACKUP_DIR'] = 'asdf' + os.environ["MACPREFS_BACKUP_DIR"] = "asdf" backup_dir = config.get_macprefs_dir() - del os.environ['MACPREFS_BACKUP_DIR'] - assert 'asdf' in backup_dir + del os.environ["MACPREFS_BACKUP_DIR"] + assert "asdf" in backup_dir -@patch('config.path.exists') -@patch('config.makedirs') +@patch("config.path.exists") +@patch("config.makedirs") def test_get_macprefs_dir_creates_if_not_exists(exists_mock, makedirs_mock): exists_mock.return_value = False config.get_macprefs_dir() @@ -27,23 +28,20 @@ def test_get_macprefs_dir_creates_if_not_exists(exists_mock, makedirs_mock): def test_get_preferences_dir(): - assert config.get_preferences_dir() == path.join( - config.get_home_dir(), 'Library/Preferences/') + assert config.get_preferences_dir() == path.join(config.get_home_dir(), "Library/Preferences/") def test_get_ssh_backup_dir(): - assert config.get_ssh_backup_dir() == path.join( - config.get_macprefs_dir(), 'ssh/') + assert config.get_ssh_backup_dir() == path.join(config.get_macprefs_dir(), "ssh/") def test_get_ssh_user_dir(): - assert config.get_ssh_user_dir() == path.join( - config.get_home_dir(), '.ssh/') + assert config.get_ssh_user_dir() == path.join(config.get_home_dir(), ".ssh/") def test_get_shared_file_lists_dir(): - path.join(config.get_home_dir(), 'Library/Application Support/com.apple.sharedfilelist/') + path.join(config.get_home_dir(), "Library/Application Support/com.apple.sharedfilelist/") + def test_get_app_store_preferences_dir(): - assert config.get_app_store_preferences_dir() == path.join( - config.get_home_dir(), 'Library/Containers/') + assert config.get_app_store_preferences_dir() == path.join(config.get_home_dir(), "Library/Containers/") diff --git a/tests/test_dotfiles.py b/tests/test_dotfiles.py index 3481d25..ad5d9d0 100644 --- a/tests/test_dotfiles.py +++ b/tests/test_dotfiles.py @@ -4,37 +4,33 @@ import dotfiles from config import get_dotfiles_backup_dir, get_home_dir, get_user -@patch('dotfiles.get_dot_files') -@patch('dotfiles.copy_files') + +@patch("dotfiles.get_dot_files") +@patch("dotfiles.copy_files") def test_backup(copy_files_mock, dotfiles_mock): - files = ['.no_file'] + files = [".no_file"] dotfiles_mock.return_value = files dotfiles.backup() dest = get_dotfiles_backup_dir() - copy_files_mock.assert_called_with( - files, dest - ) + copy_files_mock.assert_called_with(files, dest) -@patch('dotfiles.ensure_files_owned_by_user') -@patch('dotfiles.get_dot_files') -@patch('dotfiles.copy_files') +@patch("dotfiles.ensure_files_owned_by_user") +@patch("dotfiles.get_dot_files") +@patch("dotfiles.copy_files") def test_restore(copy_files_mock, dotfiles_mock, ensure_mock): - files = ['.no_file'] + files = [".no_file"] dest = get_home_dir() dotfiles_mock.return_value = files dotfiles.restore() - copy_files_mock.assert_called_with( - files, dest - ) - ensure_mock.assert_called_with( - get_user(), files - ) + copy_files_mock.assert_called_with(files, dest) + ensure_mock.assert_called_with(get_user(), files) + -@patch('dotfiles.path.isfile') -@patch('dotfiles.listdir') +@patch("dotfiles.path.isfile") +@patch("dotfiles.listdir") def test_get_dot_files(listdir_mock, isfile_mock): - files = ['testfile', '.no_file', '.good_file'] + files = ["testfile", ".no_file", ".good_file"] listdir_mock.return_value = files isfile_mock.return_value = True home_dir = get_home_dir() diff --git a/tests/test_internet_accounts.py b/tests/test_internet_accounts.py index e936c45..95a813f 100644 --- a/tests/test_internet_accounts.py +++ b/tests/test_internet_accounts.py @@ -1,38 +1,32 @@ from unittest.mock import patch -import internet_accounts import config +import internet_accounts + -@patch('internet_accounts.backup_internet_accounts') +@patch("internet_accounts.backup_internet_accounts") def test_backup(internet_accounts_mock): internet_accounts.backup() internet_accounts_mock.assert_called_once() -@patch('internet_accounts.restore_internet_accounts') +@patch("internet_accounts.restore_internet_accounts") def test_restore(internet_accounts_mock): internet_accounts.restore() internet_accounts_mock.assert_called_once() -@patch('internet_accounts.copy_dir') +@patch("internet_accounts.copy_dir") def test_backup_internet_accounts(copy_mock): source = config.get_internet_accounts_dir() dest = config.get_internet_accounts_backup_dir() internet_accounts.backup_internet_accounts() - copy_mock.assert_called_with( - source, dest - ) + copy_mock.assert_called_with(source, dest) -@patch('internet_accounts.ensure_dir_owned_by_user') -@patch('internet_accounts.copy_dir') +@patch("internet_accounts.ensure_dir_owned_by_user") +@patch("internet_accounts.copy_dir") def test_restore_internet_accounts(copy_mock, owned_mock): internet_accounts.restore_internet_accounts() - copy_mock.assert_called_with( - config.get_internet_accounts_backup_dir(), - config.get_internet_accounts_dir(), with_sudo=True - ) - owned_mock.assert_called_with( - config.get_internet_accounts_dir(), config.get_user() - ) + copy_mock.assert_called_with(config.get_internet_accounts_backup_dir(), config.get_internet_accounts_dir(), with_sudo=True) + owned_mock.assert_called_with(config.get_internet_accounts_dir(), config.get_user()) diff --git a/tests/test_macprefs.py b/tests/test_macprefs.py index fba966f..b61916f 100644 --- a/tests/test_macprefs.py +++ b/tests/test_macprefs.py @@ -1,36 +1,37 @@ -from io import StringIO -import sys import logging as log -from unittest.mock import patch, MagicMock +import sys +from io import StringIO +from unittest.mock import MagicMock, patch import utils # load as module -macprefs = utils.execute_module('macprefs', 'src/macprefs') +macprefs = utils.execute_module("macprefs", "src/macprefs") -@patch('sys.stdout', new_callable=StringIO) + +@patch("sys.stdout", new_callable=StringIO) def test_invoke_help(mock_stdout): try: - sys.argv = ['macprefs', '-h'] + sys.argv = ["macprefs", "-h"] # invoke as script - utils.execute_module('__main__', 'src/macprefs') - assert False, 'expected SystemExit' + utils.execute_module("__main__", "src/macprefs") + assert False, "expected SystemExit" except SystemExit as e: assert_correct_std_out(e, mock_stdout) -@patch('macprefs.invoke_func') +@patch("macprefs.invoke_func") def test_main_invokes_backup(invoke_func_mock): - sys.argv = ['macprefs', 'backup'] + sys.argv = ["macprefs", "backup"] macprefs.main() # pylint: disable=unused-variable args, kwargs = invoke_func_mock.call_args assert args[0].func == macprefs.backup -@patch('macprefs.invoke_func') +@patch("macprefs.invoke_func") def test_main_invokes_restore(invoke_func_mock): - sys.argv = ['macprefs', 'restore'] + sys.argv = ["macprefs", "restore"] macprefs.main() # pylint: disable=unused-variable args, kwargs = invoke_func_mock.call_args @@ -43,28 +44,36 @@ def test_invoke_func(): mock.func.assert_called_once() -@patch('sys.stdout', new_callable=StringIO) +@patch("sys.stdout", new_callable=StringIO) def test_invoke_no_args(mock_stdout): try: - sys.argv = ['macprefs'] + sys.argv = ["macprefs"] # invoke as script - utils.execute_module('__main__', 'src/macprefs') - assert False, 'expected SystemExit' + utils.execute_module("__main__", "src/macprefs") + assert False, "expected SystemExit" except SystemExit as e: assert_correct_std_out(e, mock_stdout) -@patch('internet_accounts.backup') -@patch('app_store_preferences.backup') -@patch('startup_items.backup') -@patch('ssh_files.backup') -@patch('dotfiles.backup') -@patch('shared_file_lists.backup') -@patch('system_preferences.backup') -@patch('preferences.backup') + +@patch("internet_accounts.backup") +@patch("app_store_preferences.backup") +@patch("startup_items.backup") +@patch("ssh_files.backup") +@patch("dotfiles.backup") +@patch("shared_file_lists.backup") +@patch("system_preferences.backup") +@patch("preferences.backup") # pylint: disable-msg=too-many-arguments -def test_backup(system_preferences_mock, preferences_mock, - shared_files_mock, dotfiles_mock, - ssh_mock, startup_mock, app_store_mock,internet_accounts_mock): +def test_backup( + system_preferences_mock, + preferences_mock, + shared_files_mock, + dotfiles_mock, + ssh_mock, + startup_mock, + app_store_mock, + internet_accounts_mock, +): macprefs.backup() system_preferences_mock.assert_called_once() preferences_mock.assert_called_once() @@ -75,18 +84,26 @@ def test_backup(system_preferences_mock, preferences_mock, app_store_mock.assert_called_once() internet_accounts_mock.assert_called_once() -@patch('internet_accounts.restore') -@patch('app_store_preferences.restore') -@patch('startup_items.restore') -@patch('ssh_files.restore') -@patch('dotfiles.restore') -@patch('shared_file_lists.restore') -@patch('system_preferences.restore') -@patch('preferences.restore') + +@patch("internet_accounts.restore") +@patch("app_store_preferences.restore") +@patch("startup_items.restore") +@patch("ssh_files.restore") +@patch("dotfiles.restore") +@patch("shared_file_lists.restore") +@patch("system_preferences.restore") +@patch("preferences.restore") # pylint: disable-msg=too-many-arguments -def test_restore(system_preferences_mock, preferences_mock, - shared_files_mock, dotfiles_mock, - ssh_mock, startup_mock, app_store_mock,internet_accounts_mock): +def test_restore( + system_preferences_mock, + preferences_mock, + shared_files_mock, + dotfiles_mock, + ssh_mock, + startup_mock, + app_store_mock, + internet_accounts_mock, +): macprefs.restore() system_preferences_mock.assert_called_once() preferences_mock.assert_called_once() @@ -100,28 +117,26 @@ def test_restore(system_preferences_mock, preferences_mock, def assert_correct_std_out(e, mock_stdout): assert e.code == 0 - assert 'usage: macprefs' in mock_stdout.getvalue() - assert 'backup preferences' in mock_stdout.getvalue() - assert 'restore preferences' in mock_stdout.getvalue() - assert 'show this help message and exit' in mock_stdout.getvalue() + assert "usage: macprefs" in mock_stdout.getvalue() + assert "backup preferences" in mock_stdout.getvalue() + assert "restore preferences" in mock_stdout.getvalue() + assert "show this help message and exit" in mock_stdout.getvalue() + -@patch('macprefs.log.basicConfig') +@patch("macprefs.log.basicConfig") def test_configure_logging(config_mock): macprefs.configure_logging(0) - config_mock.assert_called_with( - format="%(message)s", level=log.INFO - ) + config_mock.assert_called_with(format="%(message)s", level=log.INFO) + -@patch('macprefs.log.basicConfig') +@patch("macprefs.log.basicConfig") def test_configure_logging_works_with_verbose(config_mock): macprefs.configure_logging(1) - config_mock.assert_called_with( - format="%(message)s", level=log.DEBUG - ) + config_mock.assert_called_with(format="%(message)s", level=log.DEBUG) # pylint: disable=pointless-string-statement -''' @pytest.mark.integration +""" @pytest.mark.integration def test_intergration(): try: sys.argv = ['macprefs', 'backup'] @@ -140,4 +155,4 @@ def test_restore_intergration(): utils.execute_module('__main__', 'src/macprefs') assert False, 'expected SystemExit' except SystemExit as e: - assert e.code == 0 ''' + assert e.code == 0 """ diff --git a/tests/test_preferences.py b/tests/test_preferences.py index d8d5482..8e98d4d 100644 --- a/tests/test_preferences.py +++ b/tests/test_preferences.py @@ -1,28 +1,24 @@ from unittest.mock import patch -from config import get_preferences_dir, get_preferences_backup_dir, get_user + import preferences +from config import get_preferences_backup_dir, get_preferences_dir, get_user -@patch('preferences.copy_dir') +@patch("preferences.copy_dir") def test_backup(copy_dir_mock): source = get_preferences_dir() dest = get_preferences_backup_dir() preferences.backup() - copy_dir_mock.assert_called_with( - source, dest - ) + copy_dir_mock.assert_called_with(source, dest) + -@patch('preferences.restart_cfprefsd') -@patch('preferences.ensure_dir_owned_by_user') -@patch('preferences.copy_dir') +@patch("preferences.restart_cfprefsd") +@patch("preferences.ensure_dir_owned_by_user") +@patch("preferences.copy_dir") def test_restore(copy_dir_mock, ensure_mock, restart_mock): source = get_preferences_backup_dir() dest = get_preferences_dir() preferences.restore() - copy_dir_mock.assert_called_with( - source, dest, with_sudo=True - ) - ensure_mock.assert_called_with( - dest, get_user() - ) - restart_mock.assert_called_once() \ No newline at end of file + copy_dir_mock.assert_called_with(source, dest, with_sudo=True) + ensure_mock.assert_called_with(dest, get_user()) + restart_mock.assert_called_once() diff --git a/tests/test_publish.py b/tests/test_publish.py index 560d6d5..4b145bb 100644 --- a/tests/test_publish.py +++ b/tests/test_publish.py @@ -1,8 +1,9 @@ +import base64 import json import os import sys -import base64 -from unittest.mock import patch, call +from unittest.mock import call, patch + import publish import utils from version import __version__ @@ -10,120 +11,122 @@ def test_invoke_help(): old_argv = sys.argv - sys.argv = ['publish', '-test'] + sys.argv = ["publish", "-test"] # invoke as script - utils.execute_module('__main__', 'src/publish.py') + utils.execute_module("__main__", "src/publish.py") sys.argv = old_argv -@patch('publish.execute_shell') +@patch("publish.execute_shell") def test_check_for_uncommitted_files(execute_shell_mock): - execute_shell_mock.return_value = 'nothing to commit' + execute_shell_mock.return_value = "nothing to commit" publish.check_for_uncommitted_files() -@patch('publish.execute_shell') +@patch("publish.execute_shell") def test_check_for_uncommitted_files_raises_error(execute_shell_mock): - execute_shell_mock.return_value = 'asdf' + execute_shell_mock.return_value = "asdf" try: publish.check_for_uncommitted_files() - assert False, 'expecting ValueError' + assert False, "expecting ValueError" except ValueError as e: - assert 'uncommitted' in '\n'.join(e.args) + assert "uncommitted" in "\n".join(e.args) -@patch('publish.execute_shell') +@patch("publish.execute_shell") def test_create_version_tag_and_push(execute_shell_mock): - publish.create_version_tag_and_push('asdf') - calls = [ - call(['git', 'tag', 'asdf']), - call(['git', 'push', 'origin', 'HEAD', '--tags']) - ] + publish.create_version_tag_and_push("asdf") + calls = [call(["git", "tag", "asdf"]), call(["git", "push", "origin", "HEAD", "--tags"])] execute_shell_mock.assert_has_calls(calls) -@patch('urllib.request.urlretrieve') +@patch("urllib.request.urlretrieve") def test_download_tar(urllib_urlretrieve_mock): - filename = 'asdf' + filename = "asdf" publish.download_tar(filename) calls = [ - call('https://github.com/clintmod/macprefs/archive/' + filename, filename), + call("https://github.com/clintmod/macprefs/archive/" + filename, filename), ] urllib_urlretrieve_mock.assert_has_calls(calls) -@patch('publish.execute_shell') +@patch("publish.execute_shell") def test_calc_sha256(execute_shell_mock): - filename = 'asdf' - execute_shell_mock.return_value = 'sha' + filename = "asdf" + execute_shell_mock.return_value = "sha" asdf = publish.calc_sha256(filename) - assert asdf == 'sha' - calls = [ - call(['shasum', '-a', '256', filename]) - ] + assert asdf == "sha" + calls = [call(["shasum", "-a", "256", filename])] execute_shell_mock.assert_has_calls(calls) def test_create_brew_formula_file_content(): - filedata = publish.create_brew_formula_file_content('ver1', 'asdf1234') + filedata = publish.create_brew_formula_file_content("ver1", "asdf1234") assert isinstance(filedata, str) - filedata = base64.b64decode(filedata).decode('utf-8') - assert 'ver1.tar.gz' in filedata + filedata = base64.b64decode(filedata).decode("utf-8") + assert "ver1.tar.gz" in filedata assert 'sha256 "asdf1234"' in filedata -@patch('publish.urllib.request.urlopen') -@patch('publish.json.load') +@patch("publish.urllib.request.urlopen") +@patch("publish.json.load") def test_get_sha_of_old_macprefs_formula(json_load_mock, urlopen_mock): - json_load_mock.return_value = {'sha': 'asdf'} + json_load_mock.return_value = {"sha": "asdf"} sha = publish.get_sha_of_old_macprefs_formula() - urlopen_mock.assert_called_with( - 'https://api.github.com/repos/clintmod/homebrew-formulas/contents/Formula/macprefs.rb') - assert sha == 'asdf' + urlopen_mock.assert_called_with("https://api.github.com/repos/clintmod/homebrew-formulas/contents/Formula/macprefs.rb") + assert sha == "asdf" -@patch.dict(os.environ, {'MACPREFS_TOKEN': 'fake-token'}) -@patch('publish.open') -@patch('publish.execute_shell') +@patch.dict(os.environ, {"MACPREFS_TOKEN": "fake-token"}) +@patch("publish.open") +@patch("publish.execute_shell") # pylint: disable=unused-argument def test_upload_new_brew_formula(execute_shell_mock, open_mock): - execute_shell_mock.return_value = 'Status: 200 OK' - data = publish.upload_new_brew_formula('asdf', 'ver1', 'sha1') + execute_shell_mock.return_value = "Status: 200 OK" + data = publish.upload_new_brew_formula("asdf", "ver1", "sha1") json.loads(data) open_mock.assert_called_once() # pylint: disable=unused-variable args, kwargs = execute_shell_mock.call_args - assert 'curl' in args[0] - assert 'https://api.github.com/repos/clintmod/homebrew-formulas/contents/Formula/macprefs.rb' in args[0] + assert "curl" in args[0] + assert "https://api.github.com/repos/clintmod/homebrew-formulas/contents/Formula/macprefs.rb" in args[0] -@patch.dict(os.environ, {'MACPREFS_TOKEN': 'fake-token'}) +@patch.dict(os.environ, {"MACPREFS_TOKEN": "fake-token"}) def test_upload_new_brew_formula_accepts_real_formula_content(): # Regression: create_brew_formula_file_content() used to return bytes, # which raised a TypeError when concatenated into the request body here. - content = publish.create_brew_formula_file_content('ver1', 'asdf1234') - with patch('publish.open'), patch('publish.execute_shell') as execute_shell_mock: - execute_shell_mock.return_value = 'Status: 200 OK' - data = publish.upload_new_brew_formula(content, 'ver1', 'sha1') + content = publish.create_brew_formula_file_content("ver1", "asdf1234") + with patch("publish.open"), patch("publish.execute_shell") as execute_shell_mock: + execute_shell_mock.return_value = "Status: 200 OK" + data = publish.upload_new_brew_formula(content, "ver1", "sha1") parsed = json.loads(data) - assert parsed['content'] == content - -@patch('publish.verify_macprefs') -@patch('publish.download_macprefs') -@patch('publish.cleanup') -@patch('publish.upload_new_brew_formula') -@patch('publish.get_sha_of_old_macprefs_formula') -@patch('publish.create_brew_formula_file_content') -@patch('publish.calc_sha256') -@patch('publish.download_tar') -@patch('publish.create_version_tag_and_push') -@patch('publish.check_for_uncommitted_files') + assert parsed["content"] == content + + +@patch("publish.verify_macprefs") +@patch("publish.download_macprefs") +@patch("publish.cleanup") +@patch("publish.upload_new_brew_formula") +@patch("publish.get_sha_of_old_macprefs_formula") +@patch("publish.create_brew_formula_file_content") +@patch("publish.calc_sha256") +@patch("publish.download_tar") +@patch("publish.create_version_tag_and_push") +@patch("publish.check_for_uncommitted_files") # pylint: disable=R0913 -def test_main(check_commits_mock, create_version_mock, - download_tar, calc_sha256_mock, - create_brew_mock, get_sha_mock, - upload_mock, cleanup_mock, - download_mock, verify_mock): +def test_main( + check_commits_mock, + create_version_mock, + download_tar, + calc_sha256_mock, + create_brew_mock, + get_sha_mock, + upload_mock, + cleanup_mock, + download_mock, + verify_mock, +): assert publish.main() check_commits_mock.assert_called_once() create_version_mock.assert_called_once() @@ -137,35 +140,34 @@ def test_main(check_commits_mock, create_version_mock, verify_mock.assert_called_once() -@patch('publish.execute_shell') +@patch("publish.execute_shell") def test_download_macprefs(execute_shell_mock): publish.download_macprefs() - execute_shell_mock.assert_called_with( - ['brew', 'upgrade', 'macprefs'], False, '.', True) + execute_shell_mock.assert_called_with(["brew", "upgrade", "macprefs"], False, ".", True) -@patch('publish.execute_shell') +@patch("publish.execute_shell") def test_verify_macprefs(execute_shell_mock): execute_shell_mock.return_value = __version__ publish.verify_macprefs() - execute_shell_mock.assert_called_with(['macprefs', '--version']) + execute_shell_mock.assert_called_with(["macprefs", "--version"]) -@patch('publish.execute_shell') +@patch("publish.execute_shell") def test_verify_macprefs_throws_assertion_error(execute_shell_mock): - execute_shell_mock.return_value = 'asdf' + execute_shell_mock.return_value = "asdf" try: publish.verify_macprefs() - assert False, 'expected AssertionError' + assert False, "expected AssertionError" except AssertionError as e: - execute_shell_mock.assert_called_with(['macprefs', '--version']) - assert __version__ in '\n'.join(e.args) + execute_shell_mock.assert_called_with(["macprefs", "--version"]) + assert __version__ in "\n".join(e.args) -@patch('publish.glob.glob') -@patch('publish.os.remove') +@patch("publish.glob.glob") +@patch("publish.os.remove") def test_cleanup_removes_tar_gz_files(remove_mock, glob_mock): - glob_mock.return_value = 'a' + glob_mock.return_value = "a" publish.cleanup() - assert call('a') in remove_mock.mock_calls - assert call('github_request.json') in remove_mock.mock_calls + assert call("a") in remove_mock.mock_calls + assert call("github_request.json") in remove_mock.mock_calls diff --git a/tests/test_shared_file_lists.py b/tests/test_shared_file_lists.py index bd736c2..13001ac 100644 --- a/tests/test_shared_file_lists.py +++ b/tests/test_shared_file_lists.py @@ -1,32 +1,28 @@ from unittest.mock import patch -import shared_file_lists + import config +import shared_file_lists -@patch('shared_file_lists.copy_dir') +@patch("shared_file_lists.copy_dir") def test_backup_works(copy_dir_mock): shared_file_lists.backup() - copy_dir_mock.assert_called_with( - config.get_shared_file_lists_dir(), - config.get_shared_file_lists_backup_dir() - ) + copy_dir_mock.assert_called_with(config.get_shared_file_lists_dir(), config.get_shared_file_lists_backup_dir()) -@patch('shared_file_lists.exists') -@patch('shared_file_lists.copy_dir') +@patch("shared_file_lists.exists") +@patch("shared_file_lists.copy_dir") def test_backup_warns_if_not_exists(copy_dir_mock, exists_mock): exists_mock.return_value = False shared_file_lists.backup() copy_dir_mock.assert_not_called() - -@patch('shared_file_lists.ensure_dir_owned_by_user') -@patch('shared_file_lists.copy_dir') +@patch("shared_file_lists.ensure_dir_owned_by_user") +@patch("shared_file_lists.copy_dir") def test_restore_works(copy_dir_mock, owned_mock): shared_file_lists.restore() copy_dir_mock.assert_called_with( - config.get_shared_file_lists_backup_dir(), - config.get_shared_file_lists_dir(), with_sudo=True + config.get_shared_file_lists_backup_dir(), config.get_shared_file_lists_dir(), with_sudo=True ) - owned_mock.assert_called_with(config.get_shared_file_lists_dir(), config.get_user()) \ No newline at end of file + owned_mock.assert_called_with(config.get_shared_file_lists_dir(), config.get_user()) diff --git a/tests/test_ssh_files.py b/tests/test_ssh_files.py index ebd0ae5..7c7aac1 100644 --- a/tests/test_ssh_files.py +++ b/tests/test_ssh_files.py @@ -1,37 +1,34 @@ from unittest.mock import patch -from config import get_ssh_backup_dir, get_ssh_user_dir, get_user import ssh_files +from config import get_ssh_backup_dir, get_ssh_user_dir, get_user + -@patch('ssh_files.copy_dir') +@patch("ssh_files.copy_dir") def test_backup(copy_dir_mock): ssh_files.backup() - copy_dir_mock.assert_called_with( - get_ssh_user_dir(), get_ssh_backup_dir() - ) + copy_dir_mock.assert_called_with(get_ssh_user_dir(), get_ssh_backup_dir()) -@patch('ssh_files.exists') -@patch('ssh_files.copy_dir') +@patch("ssh_files.exists") +@patch("ssh_files.copy_dir") def test_backup_works_when_no_ssh_exists(copy_dir_mock, exists_mock): exists_mock.return_value = False ssh_files.backup() copy_dir_mock.assert_not_called() -@patch('ssh_files.ensure_dir_owned_by_user') -@patch('ssh_files.copy_dir') +@patch("ssh_files.ensure_dir_owned_by_user") +@patch("ssh_files.copy_dir") def test_restore(copy_dir_mock, ensure_mock): dest = get_ssh_user_dir() ssh_files.restore() - copy_dir_mock.assert_called_with( - get_ssh_backup_dir(), dest, with_sudo=True - ) + copy_dir_mock.assert_called_with(get_ssh_backup_dir(), dest, with_sudo=True) ensure_mock.assert_called_with(dest, get_user()) -@patch('ssh_files.exists') -@patch('ssh_files.copy_dir') +@patch("ssh_files.exists") +@patch("ssh_files.copy_dir") def test_restore_works_when_no_ssh_exists(copy_dir_mock, exists_mock): exists_mock.return_value = False ssh_files.restore() diff --git a/tests/test_startup_items.py b/tests/test_startup_items.py index 5aec58d..6757afa 100644 --- a/tests/test_startup_items.py +++ b/tests/test_startup_items.py @@ -1,11 +1,12 @@ from unittest.mock import patch -import startup_items import config +import startup_items + -@patch('startup_items.backup_system_daemons_agents') -@patch('startup_items.backup_system_launch_agents') -@patch('startup_items.backup_user_launch_agents') +@patch("startup_items.backup_system_daemons_agents") +@patch("startup_items.backup_system_launch_agents") +@patch("startup_items.backup_user_launch_agents") def test_backup(user_agents_mock, system_agents_mock, system_daemons_mock): startup_items.backup() user_agents_mock.assert_called_once() @@ -13,9 +14,9 @@ def test_backup(user_agents_mock, system_agents_mock, system_daemons_mock): system_daemons_mock.assert_called_once() -@patch('startup_items.restore_system_daemons_agents') -@patch('startup_items.restore_system_launch_agents') -@patch('startup_items.restore_user_launch_agents') +@patch("startup_items.restore_system_daemons_agents") +@patch("startup_items.restore_system_launch_agents") +@patch("startup_items.restore_user_launch_agents") def test_restore(user_agents_mock, system_agents_mock, system_daemons_mock): startup_items.restore() user_agents_mock.assert_called_once() @@ -23,73 +24,55 @@ def test_restore(user_agents_mock, system_agents_mock, system_daemons_mock): system_daemons_mock.assert_called_once() -@patch('startup_items.copy_dir') +@patch("startup_items.copy_dir") def test_backup_user_launch_agents(copy_mock): source = config.get_user_launch_agents_dir() dest = config.get_user_launch_agents_backup_dir() startup_items.backup_user_launch_agents() - copy_mock.assert_called_with( - source, dest - ) + copy_mock.assert_called_with(source, dest) -@patch('startup_items.copy_dir') +@patch("startup_items.copy_dir") def test_backup_system_launch_agents(copy_mock): source = config.get_system_launch_agents_dir() dest = config.get_system_launch_agents_backup_dir() startup_items.backup_system_launch_agents() - copy_mock.assert_called_with( - source, dest - ) + copy_mock.assert_called_with(source, dest) -@patch('startup_items.copy_dir') +@patch("startup_items.copy_dir") def test_backup_system_daemons_agents(copy_mock): source = config.get_system_launch_daemons_dir() dest = config.get_system_launch_daemons_backup_dir() startup_items.backup_system_daemons_agents() - copy_mock.assert_called_with( - source, dest - ) + copy_mock.assert_called_with(source, dest) -@patch('startup_items.ensure_dir_owned_by_user') -@patch('startup_items.copy_dir') +@patch("startup_items.ensure_dir_owned_by_user") +@patch("startup_items.copy_dir") def test_restore_user_launch_agents(copy_mock, owned_mock): source = config.get_user_launch_agents_backup_dir() dest = config.get_user_launch_agents_dir() startup_items.restore_user_launch_agents() - copy_mock.assert_called_with( - source, dest, with_sudo=True - ) - owned_mock.assert_called_with( - dest, config.get_user() - ) + copy_mock.assert_called_with(source, dest, with_sudo=True) + owned_mock.assert_called_with(dest, config.get_user()) -@patch('startup_items.ensure_dir_owned_by_user') -@patch('startup_items.copy_dir') +@patch("startup_items.ensure_dir_owned_by_user") +@patch("startup_items.copy_dir") def test_restore_system_launch_agents(copy_mock, owned_mock): source = config.get_system_launch_agents_backup_dir() dest = config.get_system_launch_agents_dir() startup_items.restore_system_launch_agents() - copy_mock.assert_called_with( - source, dest, with_sudo=True - ) - owned_mock.assert_called_with( - dest, 'root:wheel', '644' - ) + copy_mock.assert_called_with(source, dest, with_sudo=True) + owned_mock.assert_called_with(dest, "root:wheel", "644") -@patch('startup_items.ensure_dir_owned_by_user') -@patch('startup_items.copy_dir') +@patch("startup_items.ensure_dir_owned_by_user") +@patch("startup_items.copy_dir") def test_restore_system_daemons_agents(copy_mock, owned_mock): source = config.get_system_launch_daemons_backup_dir() dest = config.get_system_launch_daemons_dir() startup_items.restore_system_daemons_agents() - copy_mock.assert_called_with( - source, dest, with_sudo=True - ) - owned_mock.assert_called_with( - dest, 'root:wheel', '644' - ) + copy_mock.assert_called_with(source, dest, with_sudo=True) + owned_mock.assert_called_with(dest, "root:wheel", "644") diff --git a/tests/test_system_preferences.py b/tests/test_system_preferences.py index c55ef33..9452995 100644 --- a/tests/test_system_preferences.py +++ b/tests/test_system_preferences.py @@ -4,36 +4,34 @@ import system_preferences from config import get_sys_preferences_backup_dir -@patch('system_preferences.copy_dir') + +@patch("system_preferences.copy_dir") def test_backup(copy_dir_mock): source = system_preferences.get_pm_path() dest = system_preferences.get_pm_backup_path() system_preferences.backup() - copy_dir_mock.assert_called_with( - source, dest - ) + copy_dir_mock.assert_called_with(source, dest) + -@patch('system_preferences.restart_cfprefsd') -@patch('system_preferences.ensure_files_owned_by_user') -@patch('system_preferences.copy_dir') +@patch("system_preferences.restart_cfprefsd") +@patch("system_preferences.ensure_files_owned_by_user") +@patch("system_preferences.copy_dir") def test_restore(copy_dir_mock, ensure_mock, restart_mock): source = system_preferences.get_pm_backup_path() dest = system_preferences.get_pm_path() system_preferences.restore() - copy_dir_mock.assert_called_with( - source, dest, with_sudo=True - ) - ensure_mock.assert_called_with( - 'root:wheel', [dest], '644' - ) + copy_dir_mock.assert_called_with(source, dest, with_sudo=True) + ensure_mock.assert_called_with("root:wheel", [dest], "644") restart_mock.assert_called_once() + def test_get_pm_backup_path(): result = system_preferences.get_pm_backup_path() assert result == path.join(get_sys_preferences_backup_dir(), system_preferences.pm_file_name) -@patch('system_preferences.path.exists') + +@patch("system_preferences.path.exists") def test_get_pm_path(exists_mock): exists_mock.return_value = False result = system_preferences.get_pm_path() - assert 'SystemConfiguration' in result + assert "SystemConfiguration" in result diff --git a/tests/test_utils.py b/tests/test_utils.py index d935840..8015e8a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,31 +1,31 @@ -from subprocess import CalledProcessError import logging as log +from subprocess import CalledProcessError from unittest.mock import patch -import utils import config +import utils -@patch('utils.check_output') +@patch("utils.check_output") def test_execute_shell(check_output_mock): check_output_mock.side_effect = check_output_func - utils.execute_shell(['asdf']) + utils.execute_shell(["asdf"]) # pylint: disable=unused-argument def check_output_func(command, shell, cwd, stderr): length = len(command) assert length > 0 - assert command[0] == 'asdf' - return b'' + assert command[0] == "asdf" + return b"" -@patch('utils.check_output') +@patch("utils.check_output") def test_execute_shell_handles_errors(check_output_mock): check_output_mock.side_effect = check_output_error_func try: - utils.execute_shell(['expecting error']) - assert False, 'expected CalledProcessError' + utils.execute_shell(["expecting error"]) + assert False, "expected CalledProcessError" except CalledProcessError: pass @@ -33,165 +33,146 @@ def test_execute_shell_handles_errors(check_output_mock): def check_output_error_func(command, shell, cwd, stderr): raise CalledProcessError(0, command) -@patch('utils.log.root.getEffectiveLevel') -@patch('utils.execute_shell') + +@patch("utils.log.root.getEffectiveLevel") +@patch("utils.execute_shell") def test_copy_dir(execute_shell_mock, level_mock): level_mock.return_value = log.DEBUG - utils.copy_dir('src', 'dest') - execute_shell_mock.assert_called_with( - ['rsync', '-a', '-vv', 'src', 'dest'] - ) + utils.copy_dir("src", "dest") + execute_shell_mock.assert_called_with(["rsync", "-a", "-vv", "src", "dest"]) -@patch('utils.execute_shell') +@patch("utils.execute_shell") def test_copy_dir_works_with_sudo(execute_shell_mock): - utils.copy_dir('src', 'dest', with_sudo=True) - execute_shell_mock.assert_called_with( - ['sudo', 'rsync', '-a', 'src', 'dest'] - ) + utils.copy_dir("src", "dest", with_sudo=True) + execute_shell_mock.assert_called_with(["sudo", "rsync", "-a", "src", "dest"]) + -@patch('utils.log.root.getEffectiveLevel') -@patch('utils.execute_shell') +@patch("utils.log.root.getEffectiveLevel") +@patch("utils.execute_shell") def test_copy_files(execute_shell_mock, log_mock): log_mock.return_value = log.DEBUG - files = ['asdf'] + files = ["asdf"] dest = "asdf2" utils.copy_files(files, dest) print(log_mock.mock_calls) - execute_shell_mock.assert_called_with( - ['rsync', '-a', '-vv'] + files + [dest] - ) + execute_shell_mock.assert_called_with(["rsync", "-a", "-vv"] + files + [dest]) -@patch('utils.execute_shell') +@patch("utils.execute_shell") def test_change_owner(execute_shell_mock): ssh_dir = config.get_ssh_user_dir() - utils.change_owner(ssh_dir, 'clint', True) - execute_shell_mock.assert_called_with( - ['sudo', 'chown', '-R', 'clint', ssh_dir], suppress_errors=True - ) + utils.change_owner(ssh_dir, "clint", True) + execute_shell_mock.assert_called_with(["sudo", "chown", "-R", "clint", ssh_dir], suppress_errors=True) -@patch('utils.execute_shell') +@patch("utils.execute_shell") def test_change_mode(execute_shell_mock): ssh_dir = config.get_ssh_user_dir() utils.change_mode(ssh_dir, 600, True) - execute_shell_mock.assert_called_with( - ['sudo', 'chmod', '-R', '600', ssh_dir], suppress_errors=True - ) + execute_shell_mock.assert_called_with(["sudo", "chmod", "-R", "600", ssh_dir], suppress_errors=True) -@patch('utils.execute_shell') +@patch("utils.execute_shell") def test_ensure_subdirs_listable(execute_shell_mock): ssh_dir = config.get_ssh_user_dir() utils.ensure_subdirs_listable(config.get_ssh_user_dir()) - execute_shell_mock.assert_called_with( - ['sudo', 'chmod', '-R', 'a+X', ssh_dir], suppress_errors=True - ) + execute_shell_mock.assert_called_with(["sudo", "chmod", "-R", "a+X", ssh_dir], suppress_errors=True) -@patch('utils.ensure_subdirs_listable') -@patch('utils.change_owner') -@patch('utils.change_mode') +@patch("utils.ensure_subdirs_listable") +@patch("utils.change_owner") +@patch("utils.change_mode") def test_ensure_dir_owned_by_user(chmod_mock, chown_mock, listable_mock): dest = config.get_ssh_user_dir - utils.ensure_dir_owned_by_user(dest, 'clint') - chmod_mock.assert_called_with(dest, '600') - chown_mock.assert_called_with(dest, 'clint') + utils.ensure_dir_owned_by_user(dest, "clint") + chmod_mock.assert_called_with(dest, "600") + chown_mock.assert_called_with(dest, "clint") listable_mock.assert_called_with(dest) -@patch('utils.change_owner_for_files') -@patch('utils.change_mode_for_files') +@patch("utils.change_owner_for_files") +@patch("utils.change_mode_for_files") def test_ensure_files_owned_by_user(mode_mock, owner_mock): - files = ['.no_file'] - mode = '622' + files = [".no_file"] + mode = "622" user = config.get_user() utils.ensure_files_owned_by_user(user, files, mode) mode_mock.assert_called_with(files, mode) owner_mock.assert_called_with(files, user) -@patch('utils.execute_shell') +@patch("utils.execute_shell") def test_change_owner_for_files(shell_mock): - files = ['.no_file'] + files = [".no_file"] user = config.get_user() utils.change_owner_for_files(files, user) - shell_mock.assert_called_with( - ['sudo', 'chown', user] + files, suppress_errors=True - ) + shell_mock.assert_called_with(["sudo", "chown", user] + files, suppress_errors=True) -@patch('utils.execute_shell') +@patch("utils.execute_shell") def test_change_mode_for_files(shell_mock): - files = ['.no_file'] - mode = '622' + files = [".no_file"] + mode = "622" utils.change_mode_for_files(files, mode) - shell_mock.assert_called_with( - ['sudo', 'chmod', mode] + files, suppress_errors=True - ) + shell_mock.assert_called_with(["sudo", "chmod", mode] + files, suppress_errors=True) -@patch('utils.log.warning') -@patch('utils.execute_shell') +@patch("utils.log.warning") +@patch("utils.execute_shell") def test_run_rsync_tolerates_partial_transfer(shell_mock, warning_mock): # exit 23 (partial transfer) and 24 (files vanished) must not crash a backup for code in (23, 24): - shell_mock.side_effect = CalledProcessError(code, ['rsync'], output='partial') - utils.run_rsync(['rsync', '-a', 'src', 'dest']) + shell_mock.side_effect = CalledProcessError(code, ["rsync"], output="partial") + utils.run_rsync(["rsync", "-a", "src", "dest"]) assert warning_mock.call_count == 2 -@patch('utils.execute_shell') +@patch("utils.execute_shell") def test_run_rsync_reraises_other_errors(shell_mock): - shell_mock.side_effect = CalledProcessError(1, ['rsync']) + shell_mock.side_effect = CalledProcessError(1, ["rsync"]) try: - utils.run_rsync(['rsync', '-a', 'src', 'dest']) - assert False, 'expected CalledProcessError' + utils.run_rsync(["rsync", "-a", "src", "dest"]) + assert False, "expected CalledProcessError" except CalledProcessError: pass -@patch('utils.log.warning') -@patch('utils.execute_shell') +@patch("utils.log.warning") +@patch("utils.execute_shell") def test_run_permission_change_tolerates_failure(shell_mock, warning_mock): # chown/chmod on a security-tool-protected file must warn, not abort restore - shell_mock.return_value = 'chown: /protected: Operation not permitted' - utils.run_permission_change(['sudo', 'chown', 'clint', '/protected']) - shell_mock.assert_called_with( - ['sudo', 'chown', 'clint', '/protected'], suppress_errors=True - ) + shell_mock.return_value = "chown: /protected: Operation not permitted" + utils.run_permission_change(["sudo", "chown", "clint", "/protected"]) + shell_mock.assert_called_with(["sudo", "chown", "clint", "/protected"], suppress_errors=True) warning_mock.assert_called() -@patch('utils.log.warning') -@patch('utils.execute_shell') +@patch("utils.log.warning") +@patch("utils.execute_shell") def test_run_permission_change_quiet_on_success(shell_mock, warning_mock): - shell_mock.return_value = '' - utils.run_permission_change(['sudo', 'chown', 'clint', '/ok']) + shell_mock.return_value = "" + utils.run_permission_change(["sudo", "chown", "clint", "/ok"]) warning_mock.assert_not_called() -@patch('utils.execute_shell') +@patch("utils.execute_shell") def test_restart_cfprefsd(execute_shell_mock): utils.restart_cfprefsd() - execute_shell_mock.assert_called_with( - ['sudo', 'killall', 'cfprefsd'], suppress_errors=True - ) + execute_shell_mock.assert_called_with(["sudo", "killall", "cfprefsd"], suppress_errors=True) def test_is_none_or_empty_string(): - assert utils.is_none_or_empty_string('') + assert utils.is_none_or_empty_string("") assert utils.is_none_or_empty_string(None) - assert not utils.is_none_or_empty_string('asdf') + assert not utils.is_none_or_empty_string("asdf") + -@patch('utils.log.root.getEffectiveLevel') +@patch("utils.log.root.getEffectiveLevel") @patch("utils.execute_shell") def test_copy_file(shell_mock, level_mock): level_mock.return_value = log.DEBUG fle = "asdf" dest = "wtf" utils.copy_file(fle, dest) - shell_mock.assert_called_with( - ['rsync', '-a', '-vv', fle, dest] - ) + shell_mock.assert_called_with(["rsync", "-a", "-vv", fle, dest]) diff --git a/uv.lock b/uv.lock index 1fdfbfc..d8222dd 100644 --- a/uv.lock +++ b/uv.lock @@ -19,6 +19,129 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, ] +[[package]] +name = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, +] + +[[package]] +name = "cachecontrol" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, +] + +[package.optional-dependencies] +filecache = [ + { name = "filelock" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -131,6 +254,31 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cyclonedx-python-lib" +version = "11.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "license-expression" }, + { name = "packageurl-python" }, + { name = "py-serializable" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/c9/5d0ccdd19bc7d8ab803b90695c1706aa2ea8529685d18e682dc2524d2630/cyclonedx_python_lib-11.11.0.tar.gz", hash = "sha256:4b3194db72b613717f2912447e67ab618c75ff7dcac6c4af3c0e9e1ac617c102", size = 1442983, upload-time = "2026-06-17T11:57:49.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/f3/56ccb2884aaa3db5622368e5191a3384b15f35392aa93df8b2f508c660d2/cyclonedx_python_lib-11.11.0-py3-none-any.whl", hash = "sha256:3049fc83e06a059b5c5907a527625a8ed5073caab10607ed4c9e5503b590fd44", size = 528689, upload-time = "2026-06-17T11:57:47.358Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "dill" version = "0.4.1" @@ -158,6 +306,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "filelock" +version = "3.30.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/b7/e82e2bf111ba6ea135062e556f2ed7f4ff1c17a6c385e95ffddd99885535/filelock-3.30.1.tar.gz", hash = "sha256:6ad1b1ec6a5e2c91e02979b7d1096e5e964d83ad8a55679bacd1d96f917fda6e", size = 176008, upload-time = "2026-07-16T16:59:22.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/5e/6060a5791d3d5b54aae21c510eb2f76a51e1b28c2122cc1e39b12237e2e7/filelock-3.30.1-py3-none-any.whl", hash = "sha256:69b98bab51bed4c86b141ef338f85ca07937573e8a7798f7fe6a31ccd94b9338", size = 93698, upload-time = "2026-07-16T16:59:20.727Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -176,6 +342,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, ] +[[package]] +name = "license-expression" +version = "30.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boolean-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, +] + [[package]] name = "macprefs" version = "1.0.27" @@ -183,22 +361,38 @@ source = { virtual = "." } [package.dev-dependencies] dev = [ + { name = "pip-audit" }, { name = "pylint" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-testmon" }, { name = "pytest-watch" }, + { name = "ruff" }, ] [package.metadata] [package.metadata.requires-dev] dev = [ + { name = "pip-audit", specifier = "==2.10.1" }, { name = "pylint", specifier = "==4.0.6" }, { name = "pytest", specifier = "==9.1.1" }, { name = "pytest-cov", specifier = "==7.1.0" }, { name = "pytest-testmon", specifier = "==2.2.0" }, { name = "pytest-watch", specifier = "==4.2.0" }, + { name = "ruff", specifier = "==0.15.22" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -210,6 +404,97 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" }, + { url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, +] + +[[package]] +name = "packageurl-python" +version = "0.17.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -219,6 +504,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "pip" +version = "26.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, +] + +[[package]] +name = "pip-api" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" }, +] + +[[package]] +name = "pip-audit" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachecontrol", extra = ["filecache"] }, + { name = "cyclonedx-python-lib" }, + { name = "packaging" }, + { name = "pip-api" }, + { name = "pip-requirements-parser" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "rich" }, + { name = "tomli" }, + { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a4/f21d5f0a0edabcbce31560b73c7c5a6f72ae87af4236fd1069c8f59a353d/pip_audit-2.10.1.tar.gz", hash = "sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc", size = 54275, upload-time = "2026-06-10T22:17:01.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/a7/b0c504148114047bd1bc9d97447453c6850ca176bb2f3c0038835994e8b7/pip_audit-2.10.1-py3-none-any.whl", hash = "sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a", size = 62023, upload-time = "2026-06-10T22:17:00.309Z" }, +] + +[[package]] +name = "pip-requirements-parser" +version = "32.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" }, +] + [[package]] name = "platformdirs" version = "4.10.0" @@ -237,6 +577,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "py-serializable" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -265,6 +617,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/da/acb2e7d4dbd2dfb792d38c0d850481f29ad7049b356d23f56c687d35203b/pylint-4.0.6-py3-none-any.whl", hash = "sha256:d11a0e1fdb7b1cd46ec5d6fc78fee8b95f28695b2d6140e5809925f61e32ea54", size = 538389, upload-time = "2026-06-14T14:43:24.873Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -322,6 +683,68 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/36/47/ab65fc1d682befc318c439940f81a0de1026048479f732e84fe714cd69c0/pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9", size = 16340, upload-time = "2018-05-20T19:52:16.194Z" } +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "tomli" version = "2.4.1" @@ -376,6 +799,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + [[package]] name = "tomlkit" version = "0.15.0" @@ -394,6 +826,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + [[package]] name = "watchdog" version = "6.0.0"