diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 00000000..0a2819e1 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,51 @@ +name: Code quality + +on: + push: + pull_request: + workflow_dispatch: + +concurrency: + group: code-quality-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + isort: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + - run: uv sync --dev + - name: isort (black profile) + run: uv run poe isort --check + + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + - run: uv sync --dev + - name: Ruff format + run: uv run poe format --check + + pylint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + - run: uv sync --dev + - name: Pylint + run: uv run poe pylint + + typos: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Typos (spell check) + uses: crate-ci/typos@v1.45.0 diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 00000000..452c84d6 --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,57 @@ +name: Conventional commits + +on: + push: + pull_request: + +concurrency: + group: conventional-commits-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + commit-messages: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install checker + run: pip install "conventional-pre-commit==4.4.0" + + - name: Verify pushed commits + if: github.event_name == 'push' + run: | + set -euo pipefail + before="${{ github.event.before }}" + after="${{ github.event.after }}" + check() { + git log -1 --format=%B "$1" > /tmp/commit-msg.txt + conventional-pre-commit /tmp/commit-msg.txt + } + if [ "$before" = "0000000000000000000000000000000000000000" ]; then + check "$after" + exit 0 + fi + for rev in $(git rev-list --no-merges "${before}..${after}"); do + check "$rev" + done + + - name: Fetch PR base (for range) + if: github.event_name == 'pull_request' + run: git fetch --no-tags origin ${{ github.event.pull_request.base.sha }} + + - name: Verify PR commits + if: github.event_name == 'pull_request' + run: | + set -euo pipefail + base="${{ github.event.pull_request.base.sha }}" + for rev in $(git rev-list --no-merges "${base}..HEAD"); do + git log -1 --format=%B "$rev" > /tmp/commit-msg.txt + conventional-pre-commit /tmp/commit-msg.txt + done diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..e28d1619 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,128 @@ +name: Tests + +on: + push: + pull_request: + workflow_dispatch: + +concurrency: + group: tests-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + - run: uv sync --dev + - name: Unit tests + run: uv run poe tests --coverage + + integration: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 120 + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + - run: uv sync --dev + + - name: Export daemon versions + run: grep -v '^#' tests/integration/daemon-versions.env | grep -v '^$' >> "$GITHUB_ENV" + + - name: Restore daemon cache + id: daemon-cache + uses: actions/cache/restore@v4 + with: + path: .cache/embit-test-src-bin-cache + key: embit-daemons-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('tests/integration/daemon-versions.env') }} + restore-keys: | + embit-daemons-v1-${{ runner.os }}-${{ runner.arch }}- + + - name: Check if daemon binaries are present + id: need_toolchain + run: | + set -euo pipefail + HOST_ID="$(uname -s)-$(uname -m)" + B="${BITCOIN_REVISION:?}"; B="${B#v}" + E="${ELEMENTS_REVISION:?}" + ROOT="${GITHUB_WORKSPACE}/.cache/embit-test-src-bin-cache" + BC="${ROOT}/${HOST_ID}/bitcoin-v${B}/bitcoind" + EL="${ROOT}/${HOST_ID}/elements-${E}/elementsd" + if [[ -f "$BC" && -f "$EL" ]]; then + echo "Both daemons found in cache." + echo "install=false" >> "$GITHUB_OUTPUT" + else + echo "install=true" >> "$GITHUB_OUTPUT" + fi + + - name: Install runtime libs (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libevent-dev \ + libboost-system-dev \ + libboost-filesystem-dev \ + libboost-thread-dev \ + libdb-dev \ + libsqlite3-dev + + - name: Install build toolchain (Linux) + if: steps.need_toolchain.outputs.install == 'true' && runner.os == 'Linux' + run: | + sudo apt-get install -y \ + build-essential \ + pkg-config \ + libtool \ + autotools-dev \ + automake \ + cmake \ + git \ + curl \ + libssl-dev \ + libboost-test-dev \ + libboost-program-options-dev \ + zlib1g-dev + + - name: Install runtime libs (macOS) + if: runner.os == 'macOS' + run: | + brew install \ + boost \ + libevent \ + openssl@3 \ + berkeley-db@4 \ + sqlite + + - name: Install build toolchain (macOS) + if: steps.need_toolchain.outputs.install == 'true' && runner.os == 'macOS' + run: | + brew install \ + autoconf \ + automake \ + libtool \ + pkg-config \ + cmake + + - name: Integration tests + run: uv run poe integration-tests + + - name: Save daemon cache + if: always() && steps.daemon-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: .cache/embit-test-src-bin-cache + key: embit-daemons-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('tests/integration/daemon-versions.env') }} diff --git a/.gitignore b/.gitignore index fbe491f3..db1795a3 100644 --- a/.gitignore +++ b/.gitignore @@ -117,11 +117,6 @@ venv.bak/ # mkdocs documentation /site -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - # Pyre type checker .pyre/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 65644462..393fafef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,81 @@ +# Two hooks (approach 1): unit-only is fast; integration still builds/restores +# daemons. +# +# Skip when needed: +# +# SKIP=conventional-pre-commit,isort,ruff-format,pylint,shellcheck,typos,unit-test,integration-test git commit ... +# +# Typos uses --force-exclude so _typos.toml extend-exclude (tests, wordlists, +# i18n fixtures) applies when hooks pass explicit paths. +# +# Mirrors .github/workflows/code-quality.yml +# (isort → ruff → pylint → shellcheck → typos) and tests.yml. +# +# After changing this file: pre-commit install --install-hooks (installs +# pre-commit + commit-msg hooks). +default_install_hook_types: + - pre-commit + - commit-msg + repos: -- repo: https://github.com/psf/black - rev: 22.3.0 + - repo: https://github.com/PyCQA/isort + rev: 6.0.1 hooks: - - id: black - language_version: python3 + - id: isort + name: isort (black profile) + args: [--settings-path=pyproject.toml] + files: ^src/embit/ + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.12 + hooks: + - id: ruff-format + files: ^(src/embit|tests)/ + + - repo: https://github.com/pylint-dev/pylint + rev: v3.3.8 + hooks: + - id: pylint + name: pylint + args: [--errors-only, --rcfile=pyproject.toml] + additional_dependencies: [] + files: ^src/embit/ + + - repo: https://github.com/crate-ci/typos + rev: v1.45.0 + hooks: + - id: typos + name: typos (spell check) + args: [--force-exclude] + + - repo: local + hooks: + - id: test-clean + name: test-clean + entry: python -c 'import shutil, os; os.path.exists("htmlcov") and shutil.rmtree("htmlcov")' + language: system + pass_filenames: false + always_run: true + require_serial: true + - id: unit-test + name: unit-test + entry: uv run poe tests + language: system + pass_filenames: false + always_run: true + require_serial: true + - id: integration-test + name: integration-test + entry: bash -c 'ulimit -n 10240 2>/dev/null; uv run poe integration-tests' + language: system + pass_filenames: false + always_run: true + require_serial: true + + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v4.4.0 + hooks: + - id: conventional-pre-commit + name: conventional-commit-1.0 + stages: [commit-msg] + args: [] diff --git a/README.md b/README.md index 5237a854..08ad5879 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,10 @@ PyPi installation includes prebuilt libraries for common platforms (win, macos, If you want to build the lib yourself, see: [Building secp256k1 for `embit`](/secp256k1/README.md). - ## Using non-English BIP39 wordlists + [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md) defines wordlists for: + * English * Japanese * Korean @@ -45,6 +46,7 @@ If you want to build the lib yourself, see: [Building secp256k1 for `embit`](/se `embit` assumes English and does not include the other wordlists in order to keep this as slim as possible. However, you can override this default by providing an alternate wordlist to any of the mnemonic-handling methods: + ``` spanish_wordlist = [ "ábaco", @@ -66,25 +68,115 @@ mnemonic_to_bytes(mnemonic, wordlist=spanish_wordlist) mnemonic_from_bytes(bytes_data, wordlist=spanish_wordlist) ``` - # Development -Install in developer mode with dev dependencies: +Install [uv](https://docs.astral.sh/uv/), then sync dev dependencies (see `pyproject.toml`): + +```bash +uv sync --dev +``` + +Run tooling via Poe tasks (same layout as the `embln` reference project): + +```bash +uv run poe tests # unit tests +uv run poe integration-tests # chain daemons + integration tests +uv run poe isort +uv run poe format +uv run poe pylint +``` + +## Building + +Build the package (outputs to `dist/`): + +```bash +uv build +``` + +Install from the built wheel: + +```bash +pip install dist/embit-*.whl +``` + +The integration tests will compile `bitcoind` and `elementsd` in `/tmp`. You'll +need only install `berkeley-db@4` for `elementsd`. + +## Pre-commit tools + +Before commit your changes, perform some `pre-commit` check +(formatting, linting and test). If you do a commit without this check, you'll +need to wait a little (because it will run anyways). + +First assert that you have `pre-commit` on your PATH (via `uv sync --dev`): ```sh -pip install -e .[dev] +uv run pre-commit install --hook-type pre-commit --hook-type commit-msg +``` + +Then run: + +```bash +# First run git add to update the checklist +git add <...> + +# Pre-commit hooks +uv run pre-commit run --all-files + +# The command above will run automatically when committing: +git commit -m <...> -S + +# If you need to do a lot of rebases +# we recommend that you commit without verifying: +git commit -m <...> -S --no-verify ``` -Install pre-commit hook: +The commit commands will trigger some code quality checks: + +- `isort` (sort python imports); +- `ruff format` (formatting); +- `pylint` (linter); +- `unit-tests` (`poe tests`); +- `integration-tests` (validate `embit` against `bitcoin-core` and + `elements-core`); +- `conventional-commits` (validate commit messages). + +## Tests + +Unit tests: ```sh -pre-commit install +uv run poe tests ``` -Run tests with desktop python: +## Building + +Build the package (outputs to `dist/`): + +```bash +poetry build +``` + +Install from the built wheel: + +```bash +pip install dist/embit-*.whl +``` + +The integration tests will compile `bitcoind` and `elementsd` in `/tmp`. You'll +need only install `berkeley-db@4` for `elementsd`. + +## Pre-commit tools + +Before commit your changes, perform some `pre-commit` check +(formatting, linting and test). If you do a commit without this check, you'll +need to wait a little (because it will run anyways). + +First assert that you have `pre-commit` dependencies installed: ```sh -pytest +uv run poe integration-tests ``` Run tests with micropython: diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 00000000..7efe77b4 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,26 @@ +# https://github.com/crate-ci/typos — spell check (Rust). Run: typos +# +# Excluded paths: test vectors, non-English BIP39 fixtures, bundled wordlists, +# prebuilt binaries. Typos assumes English; i18n wordlists are not typos. +# +# Pre-commit passes explicit filenames; use `typos --force-exclude` there (see +# .pre-commit-config.yaml) or excludes in this file are ignored for those paths. + +[files] +extend-exclude = [ + "tests/**", + "examples/**", + "src/embit/wordlists/**", + "src/embit/util/prebuilt/**", +] + +[default.extend-words] +# Bitcoin wire / miniscript tokens +ser = "ser" +nd = "nd" +# Legacy API name (TODO: deprecate in follow-up) +hardend = "hardend" + +[default] +# xpub fixture strings contain "Fo" as a substring (e.g. …L2nFo17Xc). +extend-ignore-re = ["L2nFo17Xc"] diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 5e790de5..166ce7fb 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -3,4 +3,4 @@ * [Overview](/README.md) * [Tutorial](/tutorial/README.md) * [API docs](/api/README.md) -* [Recepies](/recepies/README.md) \ No newline at end of file +* [Recipes](/recipes/README.md) diff --git a/docs/api/README.md b/docs/api/README.md index cdba1aa1..6a5b82fc 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -41,7 +41,7 @@ pub = PublicKey.from_string("02018a768541c946e907bd6961f403edd820e76cddb40cefb3c ## Modules -Library is splitted into modules, list of modules sorted by topic: +Library is split into modules, list of modules sorted by topic: **Keys:** diff --git a/docs/api/_sidebar.md b/docs/api/_sidebar.md index 0dabee59..38df92f2 100644 --- a/docs/api/_sidebar.md +++ b/docs/api/_sidebar.md @@ -18,4 +18,4 @@ - [hashes](/api/hashes.md) - [liquid](/api/liquid/README.md) - [slip39](/api/slip39.md) -- [Recepies](/recepies/README.md) \ No newline at end of file +- [Recipes](/recipes/README.md) \ No newline at end of file diff --git a/docs/api/bip32.md b/docs/api/bip32.md index 8293c016..710c0855 100644 --- a/docs/api/bip32.md +++ b/docs/api/bip32.md @@ -65,7 +65,7 @@ Class method, converts a seed to an instance of `HDKey` (private key). **Arguments** - `seed` - bytes sequence to use. Normally `32`-byte or `64`-byte seeds are used. -- `version` - version to use, by default it's `NETOWRKS['main']['xprv']` +- `version` - version to use, by default it's `NETWORKS['main']['xprv']` **Returns** @@ -328,7 +328,7 @@ Converts list of indexes to human-readable path. **Arguments** - `path` - a list of indexes -- `fingerprint = None` - `4`-byte fingeprint to prepend to the derivation path +- `fingerprint = None` - `4`-byte fingerprint to prepend to the derivation path **Returns** diff --git a/docs/api/ec/README.md b/docs/api/ec/README.md index 40501bb6..34a7c265 100644 --- a/docs/api/ec/README.md +++ b/docs/api/ec/README.md @@ -42,7 +42,7 @@ print(pub) # sign a message msg = b"5"*32 # should be a 32-byte hash of the message -# ECDSA siganture: +# ECDSA signature: sig = pk.sign(msg) # serialization - DER encoding # string repr - hex of DER serialization diff --git a/docs/api/ec/_sidebar.md b/docs/api/ec/_sidebar.md index 873bc636..054bade8 100644 --- a/docs/api/ec/_sidebar.md +++ b/docs/api/ec/_sidebar.md @@ -22,4 +22,4 @@ - [hashes](/api/hashes.md) - [liquid](/api/liquid/README.md) - [slip39](/api/slip39.md) -- [Recepies](/recepies/README.md) \ No newline at end of file +- [Recipes](/recipes/README.md) \ No newline at end of file diff --git a/docs/api/ec/public_key.md b/docs/api/ec/public_key.md index 96dddb8f..177081ff 100644 --- a/docs/api/ec/public_key.md +++ b/docs/api/ec/public_key.md @@ -8,7 +8,7 @@ Individual public key class. PublicKey(point, compressed=True)` ``` -> This constructor is not very useful, probably better to instatiate this class by parsing serialized public key or creating it from private key. +> This constructor is not very useful, probably better to instantiate this class by parsing serialized public key or creating it from private key. - `point` - `64`-bytes internal representation of a point used by `libsecp256k1`. - `compressed = True` - whether to use compressed or uncompressed public keys. For any modern applications always use compressed keys (default). diff --git a/docs/api/ec/schnorr_sig.md b/docs/api/ec/schnorr_sig.md index dbc7cf18..59aa3e40 100644 --- a/docs/api/ec/schnorr_sig.md +++ b/docs/api/ec/schnorr_sig.md @@ -29,7 +29,7 @@ pub.schnorr_verify(sig, msg) SchnorrSig(sig)` ``` -> This constructor is not very useful, probably better to instatiate this class by parsing serialized signature or creating it using private key `.schnorr_sign()` method. +> This constructor is not very useful, probably better to instantiate this class by parsing serialized signature or creating it using private key `.schnorr_sign()` method. - `sig` - `64`-bytes internal representation of a Schnorr signature used by `libsecp256k1`. diff --git a/docs/api/ec/signature.md b/docs/api/ec/signature.md index 7c5a6683..8304b5bc 100644 --- a/docs/api/ec/signature.md +++ b/docs/api/ec/signature.md @@ -27,7 +27,7 @@ pub.verify(sig, msg) Signature(sig)` ``` -> This constructor is not very useful, probably better to instatiate this class by parsing serialized signature or creating it using private key `.sign()` method. +> This constructor is not very useful, probably better to instantiate this class by parsing serialized signature or creating it using private key `.sign()` method. - `sig` - `64`-bytes internal representation of a ECDSA signature used by `libsecp256k1`. diff --git a/docs/api/hashes.md b/docs/api/hashes.md index 4312f0da..062d5a38 100644 --- a/docs/api/hashes.md +++ b/docs/api/hashes.md @@ -85,7 +85,7 @@ Different tags are used for different application, for example in taproot `"BIP0 This allows reusing of the hash function without worrying about getting the same hashes for different applications. -Internaly does `sha256(sha256(tag) + sha256(tag) + data)` +Internally does `sha256(sha256(tag) + sha256(tag) + data)` **Arguments** @@ -105,7 +105,7 @@ Initializes a tagged hash object for more data. - `tag` - a tag string uniquie for your application - `msg` - first bytes to hash, `b""` by default -**Retuns** +**Returns** a hash object that can be used to `.update()` with more data and `digest()` at the end. diff --git a/docs/recepies/README.md b/docs/recepies/README.md deleted file mode 100644 index b1bcb8f7..00000000 --- a/docs/recepies/README.md +++ /dev/null @@ -1 +0,0 @@ -# Recepies \ No newline at end of file diff --git a/docs/recipes/README.md b/docs/recipes/README.md new file mode 100644 index 00000000..6c227696 --- /dev/null +++ b/docs/recipes/README.md @@ -0,0 +1 @@ +# Recipes \ No newline at end of file diff --git a/examples/change.py b/examples/change.py index df508779..b843f060 100644 --- a/examples/change.py +++ b/examples/change.py @@ -5,6 +5,7 @@ - checks that change output is from the same wallet as well - prints out transaction information for the user """ + from embit import bip39, bip32, psbt, script, ec from binascii import a2b_base64, b2a_base64 from io import BytesIO diff --git a/examples/explorer.py b/examples/explorer.py index 37d687e2..6fd0eb46 100644 --- a/examples/explorer.py +++ b/examples/explorer.py @@ -3,6 +3,7 @@ and build PSBT transaction for signing. Requires `requests` module (`pip3 install requests`) """ + import requests from embit.descriptor import Descriptor from embit.networks import NETWORKS @@ -117,9 +118,11 @@ "witness_script": ws, "redeem_script": rs, "bip32_derivations": bip32_derivations, - "witness_utxo": TransactionOutput(utxo["value"], script_pubkey) - if d.is_segwit - else None, + "witness_utxo": ( + TransactionOutput(utxo["value"], script_pubkey) + if d.is_segwit + else None + ), } for utxo in utxoarr ] diff --git a/pyproject.toml b/pyproject.toml index b37b810d..471514d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,58 +1,399 @@ -[tool.poetry] -name = "embit" -version = "0.8.0" -description="A minimal bitcoin library for MicroPython and Python3 with a focus on embedded systems." -license="MIT" -authors= ["Stepan Snigirev "] - -[tool.poetry.urls] -repository = "https://github.com/diybitcoinhardware/embit" - -[tool.poetry.dependencies] -python = "^3.0" - [build-system] -requires = ["setuptools>=42.0", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] name = "embit" version = "0.8.0" -description="A minimal bitcoin library for MicroPython and Python3 with a focus on embedded systems." -readme = "README.md" +description = "A minimal bitcoin library for MicroPython and Python3 with a focus on embedded systems." authors = [ {name = "Stepan Snigirev", email = "snigirev.stepan@gmail.com"}, ] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">3.10,<=3.14.4" classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", ] +dependencies = [] -[project.optional-dependencies] +[project.urls] +Repository = "https://github.com/diybitcoinhardware/embit" + +# Hatchling (PEP 517): `uv build` / pip install produce wheels from src/embit/. +[tool.hatch.build.targets.wheel] +packages = ["src/embit"] + +[dependency-groups] dev = [ - "pytest", - "pytest-cov", - "requests", # for integration tests - "mypy", - "black", - "pre-commit", - "mkdocs", - "mkdocs-material", - "mkdocstrings[python]", + "poethepoet>=0.45.0", + "pytest>=9.0.3", + "pytest-cov>=7.1.0", + "ruff>=0.9.0", + "pylint>=3.3.0", + "isort>=5.13.0", + "pre-commit>=3.7.0", + "requests>=2.33.1", +] + +[tool.poe.tasks.tests] +help = "Unit tests under tests/tests/ (no chain daemons)." +cmd = "pytest ${_catch:+ -W error::${_catch}} ${_cov:+ --cache-clear --cov embit --cov-report html --cov-report xml } tests/tests ${_verbose:+ --show-capture all --capture tee-sys} ${_cov:+ --cov-context=test --cov-report term-missing}" +args = [ + { name = "_verbose", options = ["-v", "--verbose"], type = "boolean" }, + { name = "_cov", options = ["-c", "--coverage"], type = "boolean" }, + { name = "_catch", options = ["--catch"], help = "Promote a warning class to error (e.g. ResourceWarning, DeprecationWarning)." }, ] -[tool.setuptools.packages.find] -where = ["src"] +[tool.poe.tasks.prepare-integration-daemons] +help = "Build or restore bitcoind/elementsd and clean integration data under EMBIT_TEMP_DIR (does not run pytest)." +args = [{ name = "force_build", options = ["--build"], type = "boolean" }] +shell = ''' +set -euo pipefail +FORCE_BUILD=0 +case "${force_build:-}" in + 1|True|true) FORCE_BUILD=1 ;; +esac + +check_installed() { + if ! command -v "$1" &>/dev/null; then + echo "You must have $1 installed!" + exit 1 + fi +} + +check_installed_compiler() { + if command -v gcc &>/dev/null \ + || command -v clang &>/dev/null; then + return 0 + fi + echo "You must have GCC or Clang installed!" + exit 1 +} + +check_installed git + +EMBIT_PROJ_DIR=$(git rev-parse --show-toplevel) +cd "$EMBIT_PROJ_DIR" + +GIT_DESCRIBE=$(git describe --tags --always) + +if [[ -z "${EMBIT_TEMP_DIR:-}" ]]; then + export EMBIT_TEMP_DIR="/tmp/embit.${GIT_DESCRIBE}" +fi + +mkdir -p "$EMBIT_TEMP_DIR/binaries" + +BITCOIN_REV="${BITCOIN_REVISION:-30.2}" +BITCOIN_REV="${BITCOIN_REV#v}" +ELEMENTS_REV="${ELEMENTS_REVISION:-23.3.2}" +export BITCOIN_REV ELEMENTS_REV + +HOST_ID="$(uname -s)-$(uname -m)" +if [[ -n "${GITHUB_WORKSPACE:-}" ]]; then + BIN_CACHE_ROOT="${GITHUB_WORKSPACE}/.cache/embit-test-src-bin-cache" +else + BIN_CACHE_ROOT="${EMBIT_BIN_SRC_CACHE_ROOT:-/tmp/embit-test-src-bin-cache}" +fi +BIN_CACHE="${BIN_CACHE_ROOT}/${HOST_ID}" +BTC_CACHE="${BIN_CACHE}/bitcoin-v${BITCOIN_REV}/bitcoind" +ELM_CACHE="${BIN_CACHE}/elements-${ELEMENTS_REV}/elementsd" + +if [[ -n "${BUILD_BITCOIND_NPROCS:-}" ]]; then + BUILD_PARALLEL="$BUILD_BITCOIND_NPROCS" +elif command -v nproc >/dev/null 2>&1; then + BUILD_PARALLEL="$(nproc)" +elif command -v sysctl >/dev/null 2>&1; then + BUILD_PARALLEL="$(sysctl -n hw.ncpu)" +else + BUILD_PARALLEL="${BUILD_PARALLEL:-4}" +fi +export BUILD_PARALLEL + +copy_cached_binary() { + [ -f "$1" ] || return 1 + echo "Restoring $3 from cache: $1" + cp -f "$1" "$2" + chmod +x "$2" +} + +update_binary_cache() { + mkdir -p "$(dirname "$2")" + cp -f "$1" "$2" + chmod +x "$2" +} + +if [[ "$FORCE_BUILD" -eq 0 ]] \ + && [[ -f "$BTC_CACHE" ]] \ + && [[ -f "$ELM_CACHE" ]]; then + echo "Cached daemons present; skipping build." +else + check_installed_compiler + check_installed make + check_installed cmake + check_installed autoconf + check_installed automake + check_installed libtool +fi + +DEST="$EMBIT_TEMP_DIR/binaries" + +echo "Cleaning data at $EMBIT_TEMP_DIR/data" +rm -rf "$EMBIT_TEMP_DIR/data" + +echo "Ensuring bitcoind (Bitcoin Core)" +if [ "$FORCE_BUILD" -eq 1 ]; then + uv run poe build-core + update_binary_cache "$DEST/bitcoind" "$BTC_CACHE" +elif [ -f "$DEST/bitcoind" ]; then + echo "Bitcoind already at $EMBIT_TEMP_DIR, skip" +elif copy_cached_binary "$BTC_CACHE" "$DEST/bitcoind" Bitcoind; then + : +else + uv run poe build-core + update_binary_cache "$DEST/bitcoind" "$BTC_CACHE" +fi -[tool.setuptools.package-data] -embit = ["util/prebuilt/*"] +echo "Ensuring elementsd (Elements Core)" +if [ "$FORCE_BUILD" -eq 1 ]; then + uv run poe build-elements + update_binary_cache "$DEST/elementsd" "$ELM_CACHE" +elif [ -f "$DEST/elementsd" ]; then + echo "Elementsd already at $EMBIT_TEMP_DIR, skip" +elif copy_cached_binary "$ELM_CACHE" "$DEST/elementsd" Elementsd; then + : +else + uv run poe build-elements + update_binary_cache "$DEST/elementsd" "$ELM_CACHE" +fi +echo "Integration daemon binaries ready at $EMBIT_TEMP_DIR/binaries" +''' + +[tool.poe.tasks.integration-tests] +help = "prepare-integration-daemons, then pytest Bitcoin and Liquid integration slices (sets EMBIT_TEMP_DIR if unset)." +shell = """ +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +GIT_DESCRIBE=$(git describe --tags --always) +export EMBIT_TEMP_DIR="${EMBIT_TEMP_DIR:-/tmp/embit.${GIT_DESCRIBE}}" +uv run poe prepare-integration-daemons +pytest tests/integration/tests -m "integration and bitcoin" +pytest tests/integration/tests -m "integration and liquid" +rm -rf "$EMBIT_TEMP_DIR/data" +""" + +[tool.poe.tasks.build-core] +help = "Compile bitcoind into $EMBIT_TEMP_DIR/binaries/bitcoind. Requires env: EMBIT_TEMP_DIR, BITCOIN_REV (see prepare-integration-daemons / BITCOIN_REVISION), BUILD_PARALLEL." +shell = ''' +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +: "${EMBIT_TEMP_DIR:?}" +: "${BITCOIN_REV:?}" +: "${BUILD_PARALLEL:?}" +PROJ=$(pwd) + +clone_and_checkout_tag() { + url=$1 dir=$2 tag=$3 label=$4 + git clone "$url" "$dir" + cd "$dir" || exit 1 + if ! git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then + echo "${label} '${tag}' is not a valid tag." + exit 1 + fi + git checkout "$tag" +} + +rm -rf "$EMBIT_TEMP_DIR/binaries/build" +mkdir -p "$EMBIT_TEMP_DIR/binaries/build" +cd "$EMBIT_TEMP_DIR/binaries/build" || exit 1 + +echo "Building Bitcoin Core..." +clone_and_checkout_tag \ + https://github.com/bitcoin/bitcoin \ + bitcoin "v${BITCOIN_REV}" bitcoin + +if [[ "$BITCOIN_REV" =~ ^([0-9]+) ]]; then + major_version="${BASH_REMATCH[1]}" +else + major_version=999 +fi +if [ "$major_version" -ge 29 ]; then + cmake -S . -B build \ + -DENABLE_IPC=OFF \ + -DBUILD_CLI=OFF \ + -DBUILD_TESTS=OFF \ + -DENABLE_WALLET=ON \ + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DENABLE_EXTERNAL_SIGNER=OFF \ + -DINSTALL_MAN=OFF + cmake --build build \ + --target bitcoind \ + -j"${BUILD_PARALLEL}" + BTC_BIN="build/bitcoin/build/bin/bitcoind" +else + ./autogen.sh + ./configure \ + --without-gui \ + --disable-tests \ + --disable-bench + make -j"${BUILD_PARALLEL}" + BTC_BIN="build/bitcoin/src/bitcoind" +fi +mv "$EMBIT_TEMP_DIR/binaries/${BTC_BIN}" \ + "$EMBIT_TEMP_DIR/binaries/bitcoind" + +rm -rf "$EMBIT_TEMP_DIR/binaries/build" +cd "$PROJ" || exit 1 +''' + +[tool.poe.tasks.build-elements] +help = "Compile elementsd into $EMBIT_TEMP_DIR/binaries/elementsd. Requires env: EMBIT_TEMP_DIR, ELEMENTS_REV (see prepare-integration-daemons / ELEMENTS_REVISION), BUILD_PARALLEL." +shell = ''' +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +: "${EMBIT_TEMP_DIR:?}" +: "${ELEMENTS_REV:?}" +: "${BUILD_PARALLEL:?}" +PROJ=$(pwd) + +clone_and_checkout_tag() { + url=$1 dir=$2 tag=$3 label=$4 + git clone "$url" "$dir" + cd "$dir" || exit 1 + if ! git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then + echo "${label} '${tag}' is not a valid tag." + exit 1 + fi + git checkout "$tag" +} + +check_berkeley_db() { + if command -v pkg-config >/dev/null 2>&1; then + for _bdb_pc in libdb libdb-4.8 db48; do + if pkg-config --exists "${_bdb_pc}" 2>/dev/null; then + return 0 + fi + done + fi + for _bdb_prefix in \ + "/opt/homebrew/opt/berkeley-db@4" \ + "/usr/local/opt/berkeley-db@4" \ + "/opt/homebrew/opt/berkeley-db" \ + "/usr/local/opt/berkeley-db"; do + if [ -f "${_bdb_prefix}/include/db.h" ]; then + return 0 + fi + done + for _db_h in \ + /usr/include/db.h \ + /usr/include/db4/db.h \ + /usr/include/db4.8/db.h \ + /usr/include/db5.3/db.h; do + if [ -f "${_db_h}" ]; then + return 0 + fi + done + echo "elementsd needs Berkeley DB dev files." + echo " macOS: brew install berkeley-db@4" + echo " Debian: sudo apt install libdb-dev" + echo " Fedora: sudo dnf install libdb-devel" + exit 1 +} + +check_berkeley_db + +rm -rf "$EMBIT_TEMP_DIR/binaries/build" +mkdir -p "$EMBIT_TEMP_DIR/binaries/build" +cd "$EMBIT_TEMP_DIR/binaries/build" || exit 1 + +echo "Building Elements Core..." +clone_and_checkout_tag \ + https://github.com/ElementsProject/elements \ + elements "elements-${ELEMENTS_REV}" elements + +./autogen.sh +./configure \ + --disable-tests \ + --disable-bench +make -j"${BUILD_PARALLEL}" +ELM_BIN="build/elements/src/elementsd" +mv "$EMBIT_TEMP_DIR/binaries/${ELM_BIN}" \ + "$EMBIT_TEMP_DIR/binaries/elementsd" +rm -rf "$EMBIT_TEMP_DIR/binaries/build" +cd "$PROJ" || exit 1 +''' + +[tool.poe.tasks.isort] +help = "Sort imports (default). Pass --check to verify without modifying." +shell = ''' +if [ -n "${_check:-}" ]; then + isort --check-only --diff src tests +else + isort src tests +fi +''' +args = [ + { name = "_check", options = ["-c", "--check"], type = "boolean" }, +] + +[tool.poe.tasks.format] +help = "Format with ruff (default). Pass --check to verify without modifying." +shell = ''' +if [ -n "${_check:-}" ]; then + ruff format --check src tests +else + ruff format src tests +fi +''' +args = [ + { name = "_check", options = ["-c", "--check"], type = "boolean" }, +] + +[tool.poe.tasks.pylint] +help = "Run pylint over the library under src/embit/ (errors only; matches legacy CI gate)." +cmd = "pylint --errors-only src/embit" + +[tool.isort] +profile = "black" +src_paths = ["src"] +known_first_party = ["embit"] + +[tool.pylint.MASTER] +init-hook = "import sys; sys.path.insert(0, 'src')" +ignore-paths = ["build/.*", ".venv/.*"] + +[tool.pylint."MESSAGES CONTROL"] +disable = [ + # project convention: %s formatting (microcontroller-friendly via embit/micropython) + "consider-using-f-string", + "missing-module-docstring", + "missing-function-docstring", + "missing-class-docstring", + "redefined-outer-name", + "import-outside-toplevel", + "too-few-public-methods", + "duplicate-code", + "protected-access", + # micropython / secp256k1 / uembit are optional or platform-specific imports + "import-error", +] + +[tool.pylint.DESIGN] +max-args = 8 +max-positional-arguments = 8 +max-locals = 25 +max-attributes = 12 [tool.pytest.ini_options] -addopts = "--cov=embit" -testpaths = [ - "tests/tests", +pythonpath = [".", "src", "tests/integration"] +python_files = ["test_*.py", "*_test.py"] +markers = [ + "integration: chain RPC tests (EMBIT_TEMP_DIR; bitcoind OR elementsd per `bitcoin`/`liquid` markers)", + "bitcoin: Bitcoin Core regtest RPC", + "liquid: Elements elreg RPC", ] [tool.coverage.run] @@ -60,18 +401,7 @@ omit = [ "*/liquid/*", ] -[tool.mypy] -mypy_path = "src" -namespace_packages = true -check_untyped_defs = true -disallow_any_generics = true -ignore_missing_imports = true -no_implicit_optional = true -show_error_codes = true -strict_equality = true -warn_redundant_casts = true -warn_return_any = true -warn_unreachable = true -warn_unused_configs = true -no_implicit_reexport = false - +[tool.pylint.TYPECHECK] +generated-members = [ + "^\\.Miniscript\\.(inner_compile|PROPS|TYPE)$", +] diff --git a/setup.py b/setup.py deleted file mode 100644 index b3993c21..00000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup, find_namespace_packages - -setup() diff --git a/src/embit/base.py b/src/embit/base.py index 9dbac739..372df7ce 100644 --- a/src/embit/base.py +++ b/src/embit/base.py @@ -1,6 +1,7 @@ """Base classes""" -from io import BytesIO + from binascii import hexlify, unhexlify +from io import BytesIO class EmbitError(Exception): diff --git a/src/embit/base58.py b/src/embit/base58.py index 196a79db..1a38ec37 100644 --- a/src/embit/base58.py +++ b/src/embit/base58.py @@ -4,6 +4,7 @@ """Base58 encoding and decoding""" import binascii + from . import hashes B58_DIGITS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" diff --git a/src/embit/bech32.py b/src/embit/bech32.py index 24ee7fa7..686a86ab 100644 --- a/src/embit/bech32.py +++ b/src/embit/bech32.py @@ -19,6 +19,7 @@ # THE SOFTWARE. """Reference implementation for Bech32 and segwit addresses.""" + from .misc import const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" diff --git a/src/embit/bip32.py b/src/embit/bip32.py index 6895afc4..fd43277b 100644 --- a/src/embit/bip32.py +++ b/src/embit/bip32.py @@ -1,12 +1,11 @@ -from . import ec -from .base import EmbitKey, EmbitError -from .misc import copy, const, secp256k1 -from .networks import NETWORKS -from . import base58 -from . import hashes import hmac from binascii import hexlify +from . import base58, ec, hashes +from .base import EmbitError, EmbitKey +from .misc import const, copy, secp256k1 +from .networks import NETWORKS + HARDENED_INDEX = const(0x80000000) diff --git a/src/embit/bip39.py b/src/embit/bip39.py index 18e55f40..bf18fa1c 100644 --- a/src/embit/bip39.py +++ b/src/embit/bip39.py @@ -1,7 +1,7 @@ -# Mnemonic convertion to seed and to/from bytes +# Mnemonic conversion to seed and to/from bytes import hashlib -from .misc import const +from .misc import const from .wordlists.bip39 import WORDLIST PBKDF2_ROUNDS = const(2048) diff --git a/src/embit/bip85.py b/src/embit/bip85.py index 97013e5e..fdcc5ff3 100644 --- a/src/embit/bip85.py +++ b/src/embit/bip85.py @@ -1,6 +1,7 @@ +import hmac + from . import bip32, bip39, ec from .bip32 import HARDENED_INDEX -import hmac BIP85_MAGIC = b"bip-entropy-from-k" diff --git a/src/embit/compact.py b/src/embit/compact.py index 0138f394..4a62523f 100644 --- a/src/embit/compact.py +++ b/src/embit/compact.py @@ -1,4 +1,5 @@ -""" Compact Int parsing / serialization """ +"""Compact Int parsing / serialization""" + import io diff --git a/src/embit/descriptor/__init__.py b/src/embit/descriptor/__init__.py index 600296d3..37d53e50 100644 --- a/src/embit/descriptor/__init__.py +++ b/src/embit/descriptor/__init__.py @@ -1,3 +1,3 @@ from . import miniscript -from .descriptor import Descriptor from .arguments import Key +from .descriptor import Descriptor diff --git a/src/embit/descriptor/arguments.py b/src/embit/descriptor/arguments.py index 0c8d4d52..3b7c891d 100644 --- a/src/embit/descriptor/arguments.py +++ b/src/embit/descriptor/arguments.py @@ -1,9 +1,10 @@ from binascii import hexlify, unhexlify -from .base import DescriptorBase -from .errors import ArgumentError -from .. import bip32, ec, compact, hashes + +from .. import bip32, compact, ec, hashes from ..bip32 import HARDENED_INDEX from ..misc import read_until +from .base import DescriptorBase +from .errors import ArgumentError class KeyOrigin: @@ -103,7 +104,7 @@ def branches(self): return None @property - def has_hardend(self): + def has_hardened(self): for idx in self.indexes: if isinstance(idx, int) and idx >= HARDENED_INDEX: return True @@ -114,6 +115,11 @@ def has_hardend(self): return True return False + @property + def has_hardend(self): + """Deprecated: use has_hardened instead.""" + return self.has_hardened + @classmethod def from_string(cls, der: str, allow_hardened=False, allow_set=True): if len(der) == 0: diff --git a/src/embit/descriptor/base.py b/src/embit/descriptor/base.py index 0c203b21..07d0f0e6 100644 --- a/src/embit/descriptor/base.py +++ b/src/embit/descriptor/base.py @@ -1,4 +1,5 @@ from io import BytesIO + from ..base import EmbitBase diff --git a/src/embit/descriptor/checksum.py b/src/embit/descriptor/checksum.py index 1e487ea5..54b66e1a 100644 --- a/src/embit/descriptor/checksum.py +++ b/src/embit/descriptor/checksum.py @@ -18,7 +18,7 @@ def polymod(c: int, val: int) -> int: def checksum(desc: str) -> str: - """Calculate checksum of desciptor string""" + """Calculate checksum of descriptor string""" INPUT_CHARSET = ( "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVW" 'XYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#"\\ ' diff --git a/src/embit/descriptor/descriptor.py b/src/embit/descriptor/descriptor.py index fd3c3057..f39061d6 100644 --- a/src/embit/descriptor/descriptor.py +++ b/src/embit/descriptor/descriptor.py @@ -1,10 +1,11 @@ from io import BytesIO + from .. import script from ..networks import NETWORKS -from .errors import DescriptorError +from .arguments import Key from .base import DescriptorBase +from .errors import DescriptorError from .miniscript import Miniscript, Multi, Sortedmulti -from .arguments import Key from .taptree import TapTree @@ -220,7 +221,7 @@ def owns(self, psbt_scope): sc = self.derive(idx, branch_index=branch_idx).script_pubkey() # if derivation is found but scriptpubkey doesn't match - fail return sc == psbt_scope.script_pubkey - for pub, (leafs, der) in psbt_scope.taproot_bip32_derivations.items(): + for pub, (leaves, der) in psbt_scope.taproot_bip32_derivations.items(): # check of the fingerprints for k in self.keys: if not k.is_extended: diff --git a/src/embit/descriptor/miniscript.py b/src/embit/descriptor/miniscript.py index 9783afc1..f3225a24 100644 --- a/src/embit/descriptor/miniscript.py +++ b/src/embit/descriptor/miniscript.py @@ -1,7 +1,7 @@ from ..misc import read_until -from .errors import MiniscriptError +from .arguments import Key, KeyHash, Number, Raw20, Raw32 from .base import DescriptorBase -from .arguments import Key, KeyHash, Number, Raw32, Raw20 +from .errors import MiniscriptError class Miniscript(DescriptorBase): @@ -901,7 +901,7 @@ def inner_compile(self): def __len__(self): return len(self.arg) + 1 - + def verify(self): super().verify() if self.arg.type != "V": diff --git a/src/embit/descriptor/taptree.py b/src/embit/descriptor/taptree.py index 7f611e5e..7d1e6fe4 100644 --- a/src/embit/descriptor/taptree.py +++ b/src/embit/descriptor/taptree.py @@ -1,8 +1,8 @@ -from .errors import MiniscriptError -from .base import DescriptorBase -from .miniscript import Miniscript from ..hashes import tagged_hash from ..script import Script +from .base import DescriptorBase +from .errors import MiniscriptError +from .miniscript import Miniscript class TapLeaf(DescriptorBase): @@ -147,5 +147,5 @@ def __str__(self): return "" if isinstance(self.tree, TapLeaf): return str(self.tree) - (left, right) = self.tree + left, right = self.tree return "{%s,%s}" % (left, right) diff --git a/src/embit/ec.py b/src/embit/ec.py index b36d861d..3ade27e0 100644 --- a/src/embit/ec.py +++ b/src/embit/ec.py @@ -1,9 +1,9 @@ -from . import base58 -from . import hashes +from binascii import hexlify, unhexlify + +from . import base58, hashes +from .base import EmbitBase, EmbitError, EmbitKey from .misc import secp256k1 from .networks import NETWORKS -from .base import EmbitBase, EmbitError, EmbitKey -from binascii import hexlify, unhexlify class ECError(EmbitError): diff --git a/src/embit/finalizer.py b/src/embit/finalizer.py index cad8bc9b..df58083f 100644 --- a/src/embit/finalizer.py +++ b/src/embit/finalizer.py @@ -1,5 +1,5 @@ from . import ec -from .script import Witness, Script +from .script import Script, Witness from .transaction import Transaction diff --git a/src/embit/liquid/addresses.py b/src/embit/liquid/addresses.py index 8ae2a328..861cbd7b 100644 --- a/src/embit/liquid/addresses.py +++ b/src/embit/liquid/addresses.py @@ -1,4 +1,4 @@ -from .. import bech32, ec, script, base58 +from .. import base58, bech32, ec, script from . import blech32 from .networks import NETWORKS diff --git a/src/embit/liquid/blip32.py b/src/embit/liquid/blip32.py index 1d49471d..16acb179 100644 --- a/src/embit/liquid/blip32.py +++ b/src/embit/liquid/blip32.py @@ -1,8 +1,10 @@ """BIP-32 for blinding keys. Non-standard yet!!!""" + +import hmac import sys + from .. import bip32, ec from .networks import NETWORKS -import hmac class BlindingHDKey(bip32.HDKey): diff --git a/src/embit/liquid/descriptor.py b/src/embit/liquid/descriptor.py index 23829315..3e904495 100644 --- a/src/embit/liquid/descriptor.py +++ b/src/embit/liquid/descriptor.py @@ -1,13 +1,13 @@ from .. import ec -from ..descriptor.descriptor import Descriptor +from ..descriptor.arguments import Key from ..descriptor.base import DescriptorBase +from ..descriptor.descriptor import Descriptor from ..descriptor.errors import DescriptorError -from ..descriptor.arguments import Key -from .networks import NETWORKS -from .addresses import address -from . import slip77 -from ..hashes import tagged_hash, sha256 +from ..hashes import sha256, tagged_hash from ..misc import secp256k1 +from . import slip77 +from .addresses import address +from .networks import NETWORKS class LDescriptor(Descriptor): diff --git a/src/embit/liquid/finalizer.py b/src/embit/liquid/finalizer.py index d8faaec4..f2da8b40 100644 --- a/src/embit/liquid/finalizer.py +++ b/src/embit/liquid/finalizer.py @@ -1,6 +1,6 @@ from .. import ec -from ..script import Witness, Script -from .transaction import TxOutWitness, Proof, LTransaction +from ..script import Script, Witness +from .transaction import LTransaction, Proof, TxOutWitness def parse_multisig(sc): diff --git a/src/embit/liquid/networks.py b/src/embit/liquid/networks.py index 2fa08378..a78d003a 100644 --- a/src/embit/liquid/networks.py +++ b/src/embit/liquid/networks.py @@ -33,8 +33,8 @@ def get_network(name): }, "elementsregtest": { "name": "Liquid Regtest", - "wif": b"\xEF", - "p2pkh": b"\x6F", + "wif": b"\xef", + "p2pkh": b"\x6f", "p2sh": b"\x4b", "bp2sh": b"\x04\x4b", "bech32": "ert", @@ -54,7 +54,7 @@ def get_network(name): # config: https://liquidtestnet.com/ "liquidtestnet": { "name": "Liquid Testnet", - "wif": b"\xEF", + "wif": b"\xef", "p2pkh": b"\x24", "p2sh": b"\x13", "bp2sh": b"\x17\x13", diff --git a/src/embit/liquid/pset.py b/src/embit/liquid/pset.py index 290c59ac..b3312b9c 100644 --- a/src/embit/liquid/pset.py +++ b/src/embit/liquid/pset.py @@ -5,24 +5,26 @@ else: from ..util import secp256k1 -from .. import compact, hashes -from ..psbt import * +import gc +import hashlib from collections import OrderedDict from io import BytesIO + +from .. import compact, hashes +from ..psbt import * +from . import slip77 from .transaction import ( + LSIGHASH, + AssetIssuance, LTransaction, - LTransactionOutput, LTransactionInput, - TxOutWitness, - TxInWitness, - AssetIssuance, + LTransactionOutput, Proof, RangeProof, - LSIGHASH, + TxInWitness, + TxOutWitness, unblind, ) -from . import slip77 -import hashlib, gc class LInputScope(InputScope): @@ -351,10 +353,12 @@ def blinded_vout(self): self.value_commitment or self.value, self.script_pubkey, self.ecdh_pubkey, - None - if not self.surjection_proof - else TxOutWitness( - Proof(self.surjection_proof), RangeProof(self.range_proof) + ( + None + if not self.surjection_proof + else TxOutWitness( + Proof(self.surjection_proof), RangeProof(self.range_proof) + ) ), ) diff --git a/src/embit/liquid/psetview.py b/src/embit/liquid/psetview.py index 70dda48e..f9e191ec 100644 --- a/src/embit/liquid/psetview.py +++ b/src/embit/liquid/psetview.py @@ -1,6 +1,7 @@ +import hashlib + from ..psbtview import * from .pset import * -import hashlib def skip_commitment(stream): diff --git a/src/embit/liquid/slip77.py b/src/embit/liquid/slip77.py index bb8b7221..3d3219e7 100644 --- a/src/embit/liquid/slip77.py +++ b/src/embit/liquid/slip77.py @@ -1,4 +1,5 @@ import hmac + from ..ec import PrivateKey DOMAIN = b"Symmetric key seed" diff --git a/src/embit/liquid/transaction.py b/src/embit/liquid/transaction.py index ca7af0a5..73b778f9 100644 --- a/src/embit/liquid/transaction.py +++ b/src/embit/liquid/transaction.py @@ -1,11 +1,11 @@ -import sys +import hashlib import io -from .. import compact +import sys + +from .. import compact, hashes +from ..base import EmbitBase from ..script import Script, Witness -from .. import hashes from ..transaction import * -from ..base import EmbitBase -import hashlib if sys.implementation.name == "micropython": import secp256k1 diff --git a/src/embit/misc.py b/src/embit/misc.py index 97669ade..f7d2c744 100644 --- a/src/embit/misc.py +++ b/src/embit/misc.py @@ -1,10 +1,11 @@ """Misc utility functions used across embit""" + import sys # implementation-specific functions and libraries: if sys.implementation.name == "micropython": - from micropython import const import secp256k1 + from micropython import const else: from .util import secp256k1 diff --git a/src/embit/networks.py b/src/embit/networks.py index 6f1a5418..ea3c6ba4 100644 --- a/src/embit/networks.py +++ b/src/embit/networks.py @@ -21,9 +21,9 @@ }, "test": { "name": "Testnet", - "wif": b"\xEF", - "p2pkh": b"\x6F", - "p2sh": b"\xC4", + "wif": b"\xef", + "p2pkh": b"\x6f", + "p2sh": b"\xc4", "bech32": "tb", "xprv": b"\x04\x35\x83\x94", "xpub": b"\x04\x35\x87\xcf", @@ -39,9 +39,9 @@ }, "regtest": { "name": "Regtest", - "wif": b"\xEF", - "p2pkh": b"\x6F", - "p2sh": b"\xC4", + "wif": b"\xef", + "p2pkh": b"\x6f", + "p2sh": b"\xc4", "bech32": "bcrt", "xprv": b"\x04\x35\x83\x94", "xpub": b"\x04\x35\x87\xcf", @@ -57,9 +57,9 @@ }, "signet": { "name": "Signet", - "wif": b"\xEF", - "p2pkh": b"\x6F", - "p2sh": b"\xC4", + "wif": b"\xef", + "p2pkh": b"\x6f", + "p2sh": b"\xc4", "bech32": "tb", "xprv": b"\x04\x35\x83\x94", "xpub": b"\x04\x35\x87\xcf", diff --git a/src/embit/psbt.py b/src/embit/psbt.py index d20c88bb..9c660bf6 100644 --- a/src/embit/psbt.py +++ b/src/embit/psbt.py @@ -1,16 +1,12 @@ +from binascii import a2b_base64, b2a_base64, hexlify, unhexlify from collections import OrderedDict -from .transaction import Transaction, TransactionOutput, TransactionInput, SIGHASH -from . import compact -from . import bip32 -from . import ec -from . import hashes -from . import script -from .script import Script, Witness -from .base import EmbitBase, EmbitError - -from binascii import b2a_base64, a2b_base64, hexlify, unhexlify from io import BytesIO +from . import bip32, compact, ec, hashes, script +from .base import EmbitBase, EmbitError +from .script import Script, Witness +from .transaction import SIGHASH, Transaction, TransactionInput, TransactionOutput + class PSBTError(EmbitError): pass @@ -888,6 +884,7 @@ def sign_input_with_tapkey( inp = inp or self.inputs[input_index] if not inp.is_taproot: return 0 + # check if key is internal key pk = key.taproot_tweak(inp.taproot_merkle_root or b"") if pk.xonly() in inp.utxo.script_pubkey.data: @@ -904,9 +901,11 @@ def sign_input_with_tapkey( # no need to sign anything else return 1 counter = 0 + # negate if necessary pub = ec.PublicKey.from_xonly(key.xonly()) - # iterate over leafs and sign + + # iterate over leaves and sign for ctrl, sc in inp.taproot_scripts.items(): if pub.xonly() not in sc: continue @@ -996,7 +995,7 @@ def sign_with(self, root, sighash=SIGHASH.DEFAULT) -> int: if fingerprint: # if taproot derivations are present add them for pub in inp.taproot_bip32_derivations: - (_leafs, derivation) = inp.taproot_bip32_derivations[pub] + _leaves, derivation = inp.taproot_bip32_derivations[pub] if derivation.fingerprint == fingerprint: # Add only if not already present if (pub, derivation) not in bip32_derivations: diff --git a/src/embit/psbtview.py b/src/embit/psbtview.py index c66bcb5b..621fdc9d 100644 --- a/src/embit/psbtview.py +++ b/src/embit/psbtview.py @@ -13,27 +13,26 @@ Makes sense to run gc.collect() after processing of each scope to free memory. """ + # TODO: refactor, a lot of code is duplicated here from transaction.py from collections import OrderedDict -import hashlib -from . import compact -from . import ec -from . import script -from .script import Script, Witness -from . import hashes +from hashlib import sha256 + +from . import compact, ec, hashes, script from .psbt import ( - PSBTError, CompressMode, InputScope, OutputScope, + PSBTError, read_string, ser_string, skip_string, ) +from .script import Script, Witness from .transaction import ( - TransactionOutput, - TransactionInput, SIGHASH, + TransactionInput, + TransactionOutput, hash_amounts, hash_script_pubkeys, ) @@ -144,8 +143,8 @@ def vin(self, i): def _skip_output(self): """Seeks over one output""" self.stream.seek(8, 1) - l = compact.read_from(self.stream) - self.stream.seek(l, 1) + script_len = compact.read_from(self.stream) + self.stream.seek(script_len, 1) def vout(self, i): if i < 0 or i >= self.num_vout: @@ -341,7 +340,7 @@ def vin(self, i, compress=None): vout = int.from_bytes(v, "little") self.seek_to_scope(i) - v = self.get_value(b"\x10", from_current=True) or b"\xFF\xFF\xFF\xFF" + v = self.get_value(b"\x10", from_current=True) or b"\xff\xff\xff\xff" sequence = int.from_bytes(v, "little") return TransactionInput(txid, vout, sequence=sequence) @@ -407,7 +406,7 @@ def get_value(self, key_start, from_current=False): def hash_prevouts(self): if self._hash_prevouts is None: - h = hashlib.sha256() + h = sha256() for i in range(self.num_inputs): inp = self.vin(i) h.update(bytes(reversed(inp.txid))) @@ -417,7 +416,7 @@ def hash_prevouts(self): def hash_sequence(self): if self._hash_sequence is None: - h = hashlib.sha256() + h = sha256() for i in range(self.num_inputs): inp = self.vin(i) h.update(inp.sequence.to_bytes(4, "little")) @@ -426,7 +425,7 @@ def hash_sequence(self): def hash_outputs(self): if self._hash_outputs is None: - h = hashlib.sha256() + h = sha256() for i in range(self.num_outputs): out = self.vout(i) h.update(out.serialize()) @@ -511,34 +510,32 @@ def sighash_segwit(self, input_index, script_pubkey, value, sighash=SIGHASH.ALL) sh = SIGHASH.ALL inp = self.vin(input_index) zero = b"\x00" * 32 # for sighashes - h = hashlib.sha256() + h = sha256() h.update(self.tx_version.to_bytes(4, "little")) if anyonecanpay: h.update(zero) else: - h.update(hashlib.sha256(self.hash_prevouts()).digest()) + h.update(sha256(self.hash_prevouts()).digest()) if anyonecanpay or sh in [SIGHASH.NONE, SIGHASH.SINGLE]: h.update(zero) else: - h.update(hashlib.sha256(self.hash_sequence()).digest()) + h.update(sha256(self.hash_sequence()).digest()) h.update(bytes(reversed(inp.txid))) h.update(inp.vout.to_bytes(4, "little")) h.update(script_pubkey.serialize()) h.update(int(value).to_bytes(8, "little")) h.update(inp.sequence.to_bytes(4, "little")) if sh not in {SIGHASH.NONE, SIGHASH.SINGLE}: - h.update(hashlib.sha256(self.hash_outputs()).digest()) + h.update(sha256(self.hash_outputs()).digest()) elif sh == SIGHASH.SINGLE and input_index < self.num_outputs: h.update( - hashlib.sha256( - hashlib.sha256(self.vout(input_index).serialize()).digest() - ).digest() + sha256(sha256(self.vout(input_index).serialize()).digest()).digest() ) else: h.update(zero) h.update(self.locktime.to_bytes(4, "little")) h.update(sighash.to_bytes(4, "little")) - return hashlib.sha256(h.digest()).digest() + return sha256(h.digest()).digest() def sighash_legacy(self, input_index, script_pubkey, sighash=SIGHASH.ALL): if input_index < 0 or input_index >= self.num_inputs: @@ -551,7 +548,7 @@ def sighash_legacy(self, input_index, script_pubkey, sighash=SIGHASH.ALL): if sh == SIGHASH.SINGLE and input_index >= self.num_outputs: return b"\x00" * 31 + b"\x01" - h = hashlib.sha256() + h = sha256() h.update(self.tx_version.to_bytes(4, "little")) # ANYONECANPAY - only one input is serialized if anyonecanpay: @@ -587,7 +584,7 @@ def sighash_legacy(self, input_index, script_pubkey, sighash=SIGHASH.ALL): raise PSBTError("Invalid sighash") h.update(self.locktime.to_bytes(4, "little")) h.update(sighash.to_bytes(4, "little")) - return hashlib.sha256(h.digest()).digest() + return sha256(h.digest()).digest() def sighash(self, i, sighash=SIGHASH.ALL, input_scope=None, **kwargs): inp = self.input(i) if input_scope is None else input_scope @@ -661,7 +658,7 @@ def sign_input_with_tapkey( counter = 0 # negate if necessary pub = ec.PublicKey.from_xonly(key.xonly()) - # iterate over leafs and sign + # iterate over leaves and sign for ctrl, sc in inp.taproot_scripts.items(): if pub.xonly() not in sc: continue @@ -749,7 +746,7 @@ def sign_input( if fingerprint: # if taproot derivations are present add them for pub in inp.taproot_bip32_derivations: - (_leafs, derivation) = inp.taproot_bip32_derivations[pub] + _leaves, derivation = inp.taproot_bip32_derivations[pub] if derivation.fingerprint == fingerprint: # Add only if not already present if (pub, derivation) not in bip32_derivations: @@ -867,7 +864,7 @@ def write_to( are streams with extra per-input and per-output data that should be written to stream as well. For example they can contain signatures or extra derivations. - If compressed flag is used then only minimal number of fields will be writen: + If compressed flag is used then only minimal number of fields will be written: For psbtv0 it will have global tx and partial sigs for all inputs For psbtv2 it will have version, tx_version, locktime, per-vin data, per-vout data and partial sigs """ diff --git a/src/embit/script.py b/src/embit/script.py index 5cea7f98..7fffaed8 100644 --- a/src/embit/script.py +++ b/src/embit/script.py @@ -1,9 +1,6 @@ -from .networks import NETWORKS -from . import base58 -from . import bech32 -from . import hashes -from . import compact +from . import base58, bech32, compact, hashes from .base import EmbitBase, EmbitError +from .networks import NETWORKS SIGHASH_ALL = 1 diff --git a/src/embit/slip39.py b/src/embit/slip39.py index 86927b39..2fe5b480 100644 --- a/src/embit/slip39.py +++ b/src/embit/slip39.py @@ -1,5 +1,6 @@ -import hmac import hashlib +import hmac + from .bip39 import mnemonic_from_bytes, mnemonic_to_bytes from .misc import secure_randint from .wordlists.slip39 import SLIP39_WORDS diff --git a/src/embit/transaction.py b/src/embit/transaction.py index 6eec9b87..0c6687d9 100644 --- a/src/embit/transaction.py +++ b/src/embit/transaction.py @@ -1,9 +1,9 @@ import hashlib -from . import compact -from . import hashes + +from . import compact, hashes from .base import EmbitBase, EmbitError -from .script import Script, Witness from .misc import const +from .script import Script, Witness class TransactionError(EmbitError): diff --git a/src/embit/util/ctypes_secp256k1.py b/src/embit/util/ctypes_secp256k1.py index 8068355e..8035554e 100644 --- a/src/embit/util/ctypes_secp256k1.py +++ b/src/embit/util/ctypes_secp256k1.py @@ -1,22 +1,22 @@ -import ctypes, os +import ctypes import ctypes.util +import os import platform import threading - from ctypes import ( - cast, + CFUNCTYPE, + POINTER, byref, - c_char, c_byte, - c_int, - c_uint, + c_char, c_char_p, + c_int, c_size_t, - c_void_p, + c_uint, c_uint64, + c_void_p, + cast, create_string_buffer, - CFUNCTYPE, - POINTER, ) _lock = threading.Lock() diff --git a/src/embit/util/key.py b/src/embit/util/key.py index f573ec5b..1482e6b6 100644 --- a/src/embit/util/key.py +++ b/src/embit/util/key.py @@ -2,9 +2,10 @@ Copy-paste from key.py in bitcoin test_framework. This is a fallback option if the library can't do ctypes bindings to secp256k1 library. """ -import random -import hmac + import hashlib +import hmac +import random def TaggedHash(tag, data): diff --git a/src/embit/wordlists/base.py b/src/embit/wordlists/base.py index 97c13449..91be4ac0 100644 --- a/src/embit/wordlists/base.py +++ b/src/embit/wordlists/base.py @@ -4,7 +4,7 @@ def __init__(self, mod): def __getitem__(self, n): if isinstance(n, slice): - (start, stop, step) = ( + start, stop, step = ( n.start or 0, n.stop if n.stop is not None else len(self), n.step or 1, diff --git a/src/embit/wordlists/ubip39.py b/src/embit/wordlists/ubip39.py index 9bb3c3f8..855801b7 100644 --- a/src/embit/wordlists/ubip39.py +++ b/src/embit/wordlists/ubip39.py @@ -1,4 +1,5 @@ -from .base import WordlistBase as _WordlistBase import uembit as _uembit +from .base import WordlistBase as _WordlistBase + WORDLIST = _WordlistBase(_uembit.wordlists.bip39) diff --git a/src/embit/wordlists/uslip39.py b/src/embit/wordlists/uslip39.py index b3bebadc..d37da862 100644 --- a/src/embit/wordlists/uslip39.py +++ b/src/embit/wordlists/uslip39.py @@ -1,4 +1,5 @@ -from .base import WordlistBase as _WordlistBase import uembit as _uembit +from .base import WordlistBase as _WordlistBase + SLIP39_WORDS = _WordlistBase(_uembit.wordlists.slip39) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 00000000..01a2bc75 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,82 @@ +"""Integration RPC tests: start only the chain daemon(s) required by collected tests.""" + +from __future__ import annotations + +import logging +import os + +import pytest + +_started: dict[str, bool] = {"bitcoin": False, "liquid": False} + + +def pytest_configure(config: pytest.Config) -> None: + """Tune log levels for the embit package and integration RPC helpers.""" + if os.environ.get("EMBIT_TEST_LOG") == "1": + level = logging.DEBUG + else: + level = logging.INFO + for name in ( + "embit", + "util", + "util.bitcoin", + "util.liquid", + "util.rpc", + ): + logging.getLogger(name).setLevel(level) + + +def pytest_collection_modifyitems( + config: pytest.Config, items: list[pytest.Item] +) -> None: + if os.environ.get("EMBIT_TEMP_DIR"): + return + reason = "EMBIT_TEMP_DIR not set — run `uv run poe integration-tests`" + skip = pytest.mark.skip(reason=reason) + for item in items: + if item.get_closest_marker("integration"): + item.add_marker(skip) + + +def pytest_collection_finish(session: pytest.Session) -> None: + # pytest_sessionstart runs *before* collection, so session.items is empty + # and daemons never started. Start chain backends once items are known. + if not os.environ.get("EMBIT_TEMP_DIR"): + return + if getattr(session.config.option, "collectonly", False): + return + items = session.items + need_btc = any(item.get_closest_marker("bitcoin") for item in items) + need_liq = any(item.get_closest_marker("liquid") for item in items) + if not need_btc and not need_liq: + return + # Imported here so collecting unit tests never imports daemon binaries. + from util.bitcoin import daemon as bitcoind + from util.liquid import daemon as elementsd + + if need_btc: + bitcoind.start() + _started["bitcoin"] = True + if need_liq: + elementsd.start() + _started["liquid"] = True + + +def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: + if not os.environ.get("EMBIT_TEMP_DIR"): + return + from util.bitcoin import daemon as bitcoind + from util.liquid import daemon as elementsd + + if _started["liquid"]: + elementsd.stop() + _started["liquid"] = False + if _started["bitcoin"]: + bitcoind.stop() + _started["bitcoin"] = False + + +@pytest.fixture +def logger(request: pytest.FixtureRequest) -> logging.Logger: + """Per-test logger (node id).""" + return logging.getLogger(request.node.nodeid) diff --git a/tests/integration/daemon-versions.env b/tests/integration/daemon-versions.env new file mode 100644 index 00000000..56a71ec9 --- /dev/null +++ b/tests/integration/daemon-versions.env @@ -0,0 +1,5 @@ +# Bitcoin Core and Elements versions for integration tests. +# Busts CI daemon cache only when these change (not when pyproject.toml poe tasks change). +# Keep in sync with defaults in pyproject.toml (tool.poe.tasks.prepare-integration-daemons). +BITCOIN_REVISION=30.2 +ELEMENTS_REVISION=23.3.2 diff --git a/tests/integration/run_tests.py b/tests/integration/run_tests.py index 05e88eed..2e35ccab 100644 --- a/tests/integration/run_tests.py +++ b/tests/integration/run_tests.py @@ -1,25 +1,23 @@ # this should run with python3 +"""Backward-compatible entrypoint; runs pytest slices only (`poe prepare-integration-daemons` first, or `poe integration-tests`).""" + import sys if sys.implementation.name == "micropython": print("This file should run with python3, not micropython!") sys.exit(1) -from util.bitcoin import daemon as bitcoind -from util.liquid import daemon as elementsd -import unittest -import time +def main() -> None: + import pytest -def main(): - try: - bitcoind.start() - elementsd.start() - unittest.main("tests") - time.sleep(10) - finally: - bitcoind.stop() - elementsd.stop() + base = ["tests/integration/tests"] + extra = sys.argv[1:] + # Separate pytest processes: one chain daemon per session (matches poe integration-tests). + for expr in ("integration and bitcoin", "integration and liquid"): + code = pytest.main([*base, "-m", expr, *extra]) + if code != 0: + raise SystemExit(code) if __name__ == "__main__": diff --git a/tests/integration/tests/test_psbt.py b/tests/integration/tests/test_psbt.py index b425722e..f4884690 100644 --- a/tests/integration/tests/test_psbt.py +++ b/tests/integration/tests/test_psbt.py @@ -1,11 +1,16 @@ -from unittest import TestCase, skip -from util.bitcoin import daemon import random +from unittest import TestCase + +import pytest + from embit.descriptor import Descriptor from embit.descriptor.checksum import add_checksum from embit.bip32 import HDKey from embit.networks import NETWORKS from embit.psbt import PSBT +from util.bitcoin import daemon + +pytestmark = [pytest.mark.integration, pytest.mark.bitcoin] wallet_prefix = "test" + random.randint(0, 0xFFFFFFFF).to_bytes(4, "big").hex() root = HDKey.from_string( @@ -36,26 +41,27 @@ def sign_with_descriptor(self, d1, d2, root): # to add checksums d1 = add_checksum(str(d1)) d2 = add_checksum(str(d2)) - rpc.createwallet(wname, True, True) + # createwallet(name, disable_private_keys, blank, + # passphrase, avoid_reuse, descriptors) + rpc.createwallet(wname, True, True, "", False, True) w = daemon.wallet(wname) - res = w.importmulti( + res = w.importdescriptors( [ { "desc": d1, "internal": False, "timestamp": "now", - "watchonly": True, + "active": True, "range": 10, }, { "desc": d2, "internal": True, "timestamp": "now", - "watchonly": True, + "active": True, "range": 10, }, ], - {"rescan": False}, ) self.assertTrue(all([k["success"] for k in res])) wdefault = daemon.wallet() diff --git a/tests/integration/tests/test_pset.py b/tests/integration/tests/test_pset.py index 2f976d26..f4608e6a 100644 --- a/tests/integration/tests/test_pset.py +++ b/tests/integration/tests/test_pset.py @@ -1,18 +1,20 @@ -from unittest import TestCase, skip -from util.liquid import daemon -import random -import time import os +import random +from unittest import TestCase + +import pytest -from embit.liquid.descriptor import LDescriptor as Descriptor from embit.descriptor.checksum import add_checksum from embit.bip32 import HDKey +from embit.ec import PrivateKey +from embit.liquid.addresses import addr_decode +from embit.liquid.descriptor import LDescriptor as Descriptor +from embit.liquid.finalizer import finalize_psbt from embit.liquid.networks import get_network from embit.liquid.pset import PSET as PSBT -from embit.liquid.transaction import LSIGHASH -from embit.liquid.finalizer import finalize_psbt -from embit.liquid.addresses import addr_decode -from embit.ec import PrivateKey +from util.liquid import daemon + +pytestmark = [pytest.mark.integration, pytest.mark.liquid] wallet_prefix = "test" + random.randint(0, 0xFFFFFFFF).to_bytes(4, "big").hex() root = HDKey.from_string( diff --git a/tests/integration/util/bitcoin.py b/tests/integration/util/bitcoin.py index 0d3552ca..bb787315 100644 --- a/tests/integration/util/bitcoin.py +++ b/tests/integration/util/bitcoin.py @@ -1,24 +1,44 @@ -import subprocess +import logging import os -import time -import signal import shutil +import subprocess +import time + +import shlex + from .rpc import BitcoinRPC +logger = logging.getLogger(__name__) + class Bitcoind: - datadir = os.path.abspath("./chain/bitcoin") rpcport = 18778 port = 18779 rpcuser = "bitcoin" rpcpassword = "secret" name = "Bitcoin Core" - retry_count = 10 - binary = "bitcoind" def __init__(self): self._rpc = None self._address = None + self.proc = None + + @staticmethod + def _temp_dir() -> str: + d = os.environ.get("EMBIT_TEMP_DIR") + if not d: + raise RuntimeError( + "EMBIT_TEMP_DIR is not set. Run `uv run poe integration-tests`.", + ) + return d + + @property + def datadir(self): + return os.path.join(self._temp_dir(), "data", "bitcoin", "chain") + + @property + def binary(self): + return os.path.join(self._temp_dir(), "binaries", "bitcoind") @property def address(self): @@ -28,7 +48,12 @@ def address(self): @property def cmd(self): - return f"{self.binary} -datadir={self.datadir} -regtest -fallbackfee=0.0001 -rpcuser={self.rpcuser} -rpcpassword={self.rpcpassword} -rpcport={self.rpcport} -port={self.port}" + return ( + f"{self.binary} -datadir={self.datadir} -regtest " + f"-fallbackfee=0.0001 -rpcuser={self.rpcuser} " + f"-rpcpassword={self.rpcpassword} -rpcport={self.rpcport} " + f"-port={self.port}" + ) @property def rpc(self): @@ -40,25 +65,43 @@ def wallet(self, wname=""): return self.rpc.wallet(wname) def start(self): - print(f"Starting {self.name} in regtest mode with datadir {self.datadir}") + logger.info( + "starting name=%s datadir=%s rpcport=%s", + self.name, + self.datadir, + self.rpcport, + ) try: shutil.rmtree(self.datadir) - except: + except OSError: pass try: os.makedirs(self.datadir) - except: + except OSError: pass + self.proc = subprocess.Popen( - self.cmd, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setsid + shlex.split(self.cmd), + start_new_session=True, ) - time.sleep(1) + self._wait_for_rpc() self.get_coins() + def _wait_for_rpc(self, timeout=30): + """Poll RPC until it responds or timeout.""" + for _ in range(timeout * 2): + try: + self.rpc.getblockchaininfo() + return + except Exception: + time.sleep(0.5) + raise RuntimeError(f"{self.name} RPC not ready after {timeout}s") + def get_coins(self): # create default wallet if "" not in self.rpc.listwallets(): - self.rpc.createwallet("") + # createwallet(name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors) + self.rpc.createwallet("", False, False, "", False, True) self.mine(101) assert self.rpc.getbalance(wallet="") > 0 @@ -66,19 +109,26 @@ def mine(self, n=1): self.rpc.generatetoaddress(n, self.address) def stop(self): - print(f"Shutting down {self.name}") - os.killpg( - os.getpgid(self.proc.pid), signal.SIGTERM - ) # Send the signal to all the process groups - time.sleep(3) - for i in range(self.retry_count): - try: - # shutil.rmtree(self.datadir) - return - except Exception as e: - print(f"Exception: {e}") - print(f"Retrying in 1 second... {i}/{retry_count}") - time.sleep(1) + logger.info("stopping name=%s", self.name) + try: + self.rpc.stop() + except Exception as e: + logger.warning( + "rpc_stop_failed name=%s error=%s", + self.name, + e, + ) + if self.proc is None or self.proc.poll() is not None: + return + try: + self.proc.wait(timeout=120) + except subprocess.TimeoutExpired: + logger.warning( + "daemon_stop_timeout name=%s killing=yes", + self.name, + ) + self.proc.kill() + self.proc.wait(timeout=30) daemon = Bitcoind() diff --git a/tests/integration/util/liquid.py b/tests/integration/util/liquid.py index fada302b..e2a12b28 100644 --- a/tests/integration/util/liquid.py +++ b/tests/integration/util/liquid.py @@ -1,30 +1,45 @@ import os + from .bitcoin import Bitcoind class Elementsd(Bitcoind): - datadir = os.path.abspath("./chain/elements") rpcport = 18998 port = 18999 rpcuser = "liquid" rpcpassword = "secret" name = "Elements Core" - binary = "elementsd" + + @property + def datadir(self): + return os.path.join(self._temp_dir(), "data", "elements") + + @property + def binary(self): + return os.path.join(self._temp_dir(), "binaries", "elementsd") @property def cmd(self): - return f"{self.binary} -datadir={self.datadir} -chain=elreg -fallbackfee=0.000001 -rpcuser={self.rpcuser} -rpcpassword={self.rpcpassword} -rpcport={self.rpcport} -port={self.port} -validatepegin=0 -initialfreecoins=2100000000000000" + return ( + f"{self.binary}" + f" -datadir={self.datadir}" + f" -chain=elreg" + f" -fallbackfee=0.000001" + f" -rpcuser={self.rpcuser}" + f" -rpcpassword={self.rpcpassword}" + f" -rpcport={self.rpcport}" + f" -port={self.port}" + f" -validatepegin=0" + f" -con_blocksubsidy=5000000000" + ) def get_coins(self): # create default wallet if "" not in self.rpc.listwallets(): - self.rpc.createwallet("") - self.rpc.rescanblockchain(wallet="") - self.mine(10) - balance = self.rpc.getbalance(wallet="") - addr = self.rpc.getnewaddress(wallet="") - self.rpc.sendtoaddress(addr, balance["bitcoin"] // 2) - self.mine(1) + # createwallet(name, disable_private_keys, + # blank, passphrase, avoid_reuse, descriptors) + self.rpc.createwallet("", False, False, "", False, True) + self.mine(101) assert self.rpc.getbalance(wallet="").get("bitcoin", 0) > 0 diff --git a/tests/integration/util/rpc.py b/tests/integration/util/rpc.py index 9ebfafe7..fd1cdef1 100644 --- a/tests/integration/util/rpc.py +++ b/tests/integration/util/rpc.py @@ -1,7 +1,8 @@ +import os import logging -import requests, json, os -import os, sys, errno -import time +import requests +import json +import sys logger = logging.getLogger(__name__) @@ -322,7 +323,7 @@ def test_connection(self): def clone(self): """ Returns a clone of self. - Usefull if you want to mess with the properties + Useful if you want to mess with the properties """ return BitcoinRPC( self.user, @@ -360,6 +361,8 @@ def multi(self, calls: list, **kwargs): url = self.url if "wallet" in kwargs: url = url + "/wallet/{}".format(kwargs["wallet"]) + + logger.debug("POST %s: %s", url, payload) r = self.session.post( url, data=json.dumps(payload), headers=headers, timeout=timeout ) @@ -373,10 +376,11 @@ def multi(self, calls: list, **kwargs): def __getattr__(self, method): def fn(*args, **kwargs): - r = self.multi([(method, *args)], **kwargs)[0] - if r["error"] is not None: - raise RpcError("Request error: %s" % r["error"]["message"], r) - return r["result"] + res = self.multi([(method, *args)], **kwargs)[0] + if "error" in res and res["error"] is not None: + logger.debug("RPC error: %s", res) + raise RpcError("Request error: %s" % res["error"]["message"], res) + return res["result"] return fn diff --git a/tests/tests/test_bech32.py b/tests/tests/test_bech32.py index 7d71fb17..d78ab809 100644 --- a/tests/tests/test_bech32.py +++ b/tests/tests/test_bech32.py @@ -41,7 +41,7 @@ def segwit_scriptpubkey(witver, witprog): INVALID_CHECKSUM = [ " 1nwldj5", - "\x7F" + "1axkwrx", + "\x7f" + "1axkwrx", "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", "pzry9x0s0muk", "1pzry9x0s0muk", diff --git a/tests/tests/test_bip32.py b/tests/tests/test_bip32.py index 4a74b72d..a7c5932f 100644 --- a/tests/tests/test_bip32.py +++ b/tests/tests/test_bip32.py @@ -17,109 +17,106 @@ DERIVE_VECTORS = [ # Vector 1: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-1 - ( # Chain m + ( # Chain m SEEDS[0], [], "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", ), - ( # Chain m/0H + ( # Chain m/0H SEEDS[0], [HARD(0)], "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", ), - ( # Chain m/0H/1 + ( # Chain m/0H/1 SEEDS[0], [HARD(0), 1], "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", ), - ( # Chain m/0H/1/2H + ( # Chain m/0H/1/2H SEEDS[0], [HARD(0), 1, HARD(2)], "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", ), - ( # Chain m/0H/1/2H/2 + ( # Chain m/0H/1/2H/2 SEEDS[0], [HARD(0), 1, HARD(2), 2], "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", ), - ( # Chain m/0H/1/2H/2/1000000000 + ( # Chain m/0H/1/2H/2/1000000000 SEEDS[0], [HARD(0), 1, HARD(2), 2, 1000000000], "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", ), - # Vector 2: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-2 - ( # Chain m + ( # Chain m SEEDS[1], [], "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", ), - ( # Chain m/0 + ( # Chain m/0 SEEDS[1], [0], "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt", "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", ), - ( # Chain m/0/2147483647H + ( # Chain m/0/2147483647H SEEDS[1], [0, HARD(2147483647)], "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9", "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a", ), - ( # Chain m/0/2147483647H/1 + ( # Chain m/0/2147483647H/1 SEEDS[1], [0, HARD(2147483647), 1], "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef", "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon", ), - ( # Chain m/0/2147483647H/1/2147483646H + ( # Chain m/0/2147483647H/1/2147483646H SEEDS[1], [0, HARD(2147483647), 1, HARD(2147483646)], "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc", "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", ), - ( # Chain m/0/2147483647H/1/2147483646H/2 + ( # Chain m/0/2147483647H/1/2147483646H/2 SEEDS[1], [0, HARD(2147483647), 1, HARD(2147483646), 2], "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", ), - # Vector 3: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-3 - ( # Chain m + ( # Chain m SEEDS[2], [], "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6", "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13", ), - ( # Chain m/0H + ( # Chain m/0H SEEDS[2], [HARD(0)], "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L", "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y", ), - # Vector 4: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-4 - ( # Chain m + ( # Chain m SEEDS[3], [], "xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv", "xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa", ), - ( # Chain m/0H + ( # Chain m/0H SEEDS[3], [HARD(0)], "xprv9vB7xEWwNp9kh1wQRfCCQMnZUEG21LpbR9NPCNN1dwhiZkjjeGRnaALmPXCX7SgjFTiCTT6bXes17boXtjq3xLpcDjzEuGLQBM5ohqkao9G", "xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m", ), - ( # Chain m/0H/1H + ( # Chain m/0H/1H SEEDS[3], [HARD(0), HARD(1)], "xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1", @@ -129,67 +126,67 @@ FROM_BASE58_ERROR_VECTORS = ( # Vector 5: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#test-vector-5 - ( # Invalid (pubkey version / prvkey mismatch) + ( # Invalid (pubkey version / prvkey mismatch) "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6LBpB85b3D2yc8sfvZU521AAwdZafEz7mnzBBsz4wKY5fTtTQBm", HDError, ), - ( # Invalid (prvkey version / pubkey mismatch) + ( # Invalid (prvkey version / pubkey mismatch) "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGTQQD3dC4H2D5GBj7vWvSQaaBv5cxi9gafk7NF3pnBju6dwKvH", HDError, ), - ( # Invalid (invalid pubkey prefix 04) + ( # Invalid (invalid pubkey prefix 04) "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Txnt3siSujt9RCVYsx4qHZGc62TG4McvMGcAUjeuwZdduYEvFn", ECError, ), - ( # Invalid (invalid prvkey prefix 04) + ( # Invalid (invalid prvkey prefix 04) "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFGpWnsj83BHtEy5Zt8CcDr1UiRXuWCmTQLxEK9vbz5gPstX92JQ", ECError, ), - ( # Invalid (invalid pubkey prefix 01) + ( # Invalid (invalid pubkey prefix 01) "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6N8ZMMXctdiCjxTNq964yKkwrkBJJwpzZS4HS2fxvyYUA4q2Xe4", ECError, ), - ( # Invalid (invalid prvkey prefix 01) + ( # Invalid (invalid prvkey prefix 01) "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fEQ3Qen6J", ECError, ), - ( # Invalid (zero depth with non-zero parent fingerprint) + ( # Invalid (zero depth with non-zero parent fingerprint) "xprv9s2SPatNQ9Vc6GTbVMFPFo7jsaZySyzk7L8n2uqKXJen3KUmvQNTuLh3fhZMBoG3G4ZW1N2kZuHEPY53qmbZzCHshoQnNf4GvELZfqTUrcv", HDError, ), - ( # Invalid (zero depth with non-zero parent fingerprint) + ( # Invalid (zero depth with non-zero parent fingerprint) "xpub661no6RGEX3uJkY4bNnPcw4URcQTrSibUZ4NqJEw5eBkv7ovTwgiT91XX27VbEXGENhYRCf7hyEbWrR3FewATdCEebj6znwMfQkhRYHRLpJ", HDError, ), - ( # Invalid (zero depth with non-zero index) + ( # Invalid (zero depth with non-zero index) "xprv9s21ZrQH4r4TsiLvyLXqM9P7k1K3EYhA1kkD6xuquB5i39AU8KF42acDyL3qsDbU9NmZn6MsGSUYZEsuoePmjzsB3eFKSUEh3Gu1N3cqVUN", HDError, ), - ( # Invalid (zero depth with non-zero index) + ( # Invalid (zero depth with non-zero index) "xpub661MyMwAuDcm6CRQ5N4qiHKrJ39Xe1R1NyfouMKTTWcguwVcfrZJaNvhpebzGerh7gucBvzEQWRugZDuDXjNDRmXzSZe4c7mnTK97pTvGS8", HDError, ), - ( # Invalid (unknown extended key version) + ( # Invalid (unknown extended key version) "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHGMQzT7ayAmfo4z3gY5KfbrZWZ6St24UVf2Qgo6oujFktLHdHY4", HDError, ), - ( # Invalid (unknown extended key version) + ( # Invalid (unknown extended key version) "DMwo58pR1QLEFihHiXPVykYB6fJmsTeHvyTp7hRThAtCX8CvYzgPcn8XnmdfHPmHJiEDXkTiJTVV9rHEBUem2mwVbbNfvT2MTcAqj3nesx8uBf9", HDError, ), - ( # Invalid (private key 0 not in 1..n-1) + ( # Invalid (private key 0 not in 1..n-1) "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzF93Y5wvzdUayhgkkFoicQZcP3y52uPPxFnfoLZB21Teqt1VvEHx", ECError, ), - ( # Invalid (private key n not in 1..n-1) + ( # Invalid (private key n not in 1..n-1) "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD5SDKr24z3aiUvKr9bJpdrcLg1y3G", ECError, ), - ( # Invalid (invalid pubkey 020000000000000000000000000000000000000000000000000000000000000007) + ( # Invalid (invalid pubkey 020000000000000000000000000000000000000000000000000000000000000007) "xpub661MyMwAqRbcEYS8w7XLSVeEsBXy79zSzH1J8vCdxAZningWLdN3zgtU6Q5JXayek4PRsn35jii4veMimro1xefsM58PgBMrvdYre8QyULY", ECError, ), - ( # Invalid (invalid checksum) + ( # Invalid (invalid checksum) "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHL", ValueError, ), @@ -213,7 +210,7 @@ def test_bip32_derive(self): def test_from_base58_errors(self): for inv_xkey, exp_err in FROM_BASE58_ERROR_VECTORS: with self.assertRaises(exp_err): - HDKey.from_base58(inv_xkey) + HDKey.from_base58(inv_xkey) def test_identity(self): xkeys = [ diff --git a/tests/tests/test_bip85.py b/tests/tests/test_bip85.py index 7b6280b9..0169f8fb 100644 --- a/tests/tests/test_bip85.py +++ b/tests/tests/test_bip85.py @@ -48,12 +48,29 @@ class Bip85Test(TestCase): - def test_derive_entropy(self): for app_index, path, expected in [ - (39, [0, 12, 0], unhexlify("6250b68daf746d12a24d58b4787a714bf1b58d69e4c2a466276fb16fe93dc52b6fac6b756894072241447cad56f6405ee326dbb473d2f5e943543590082927c0")), - (2, [0], unhexlify("7040bb53104f27367f317558e78a994ada7296c6fde36a364e5baf206e502bb1f988080b7dd814e7ae7d6d83edbb6689886a560e165f4a740877cdf3beecacf8")), - (32, [0], unhexlify("52405cd0dd21c5be78314a7c1a3c65ffd8d896536cc7dee3157db5824f0c92e2ead0b33988a616cf6a497f1c169d9e92562604e38305ccd3fc96f2252c177682")), + ( + 39, + [0, 12, 0], + unhexlify( + "6250b68daf746d12a24d58b4787a714bf1b58d69e4c2a466276fb16fe93dc52b6fac6b756894072241447cad56f6405ee326dbb473d2f5e943543590082927c0" + ), + ), + ( + 2, + [0], + unhexlify( + "7040bb53104f27367f317558e78a994ada7296c6fde36a364e5baf206e502bb1f988080b7dd814e7ae7d6d83edbb6689886a560e165f4a740877cdf3beecacf8" + ), + ), + ( + 32, + [0], + unhexlify( + "52405cd0dd21c5be78314a7c1a3c65ffd8d896536cc7dee3157db5824f0c92e2ead0b33988a616cf6a497f1c169d9e92562604e38305ccd3fc96f2252c177682" + ), + ), ]: result = bip85.derive_entropy(ROOT, app_index, path) self.assertEqual(result, expected) diff --git a/tests/tests/test_descriptor.py b/tests/tests/test_descriptor.py index 2101227b..5c462c63 100644 --- a/tests/tests/test_descriptor.py +++ b/tests/tests/test_descriptor.py @@ -132,16 +132,29 @@ def test_descriptors(self): 2, ), ( - "wsh(sortedmulti(2,%s,%s,%s))" % tuple(key.replace("<0;1>", "0") for key in keys[:3]), + "wsh(sortedmulti(2,%s,%s,%s))" + % tuple(key.replace("<0;1>", "0") for key in keys[:3]), "522103801b3a4e3ca0d61d469445621561c47f6c1424d0fd353a44c2c3ebb84ae78f592103b8fa5d5959fa4027ccbf0736a86ccde4242e3051ea363437b4ff0d52598d7cec2103e7d285b4817f83f724cd29394da75dfc84fe639ed147a944e7e6064703b1413053ae", 1, ), - ("wpkh(%s)" % keys[0], "0014f8f93df2160de8fd3ca716e2f905c74da3f9839f", 2,), - ("sh(wpkh(%s))" % keys[0], "0014f8f93df2160de8fd3ca716e2f905c74da3f9839f", 2,), - ("pkh(%s)" % keys[0], "76a914f8f93df2160de8fd3ca716e2f905c74da3f9839f88ac", 2,), + ( + "wpkh(%s)" % keys[0], + "0014f8f93df2160de8fd3ca716e2f905c74da3f9839f", + 2, + ), + ( + "sh(wpkh(%s))" % keys[0], + "0014f8f93df2160de8fd3ca716e2f905c74da3f9839f", + 2, + ), + ( + "pkh(%s)" % keys[0], + "76a914f8f93df2160de8fd3ca716e2f905c74da3f9839f88ac", + 2, + ), ] - error_cases = [1,2,3,4,5,6,7,8,11] + error_cases = [1, 2, 3, 4, 5, 6, 7, 8, 11] for i, (d, a, n_branches) in enumerate(dd): if i in error_cases: self.assertRaises(DescriptorError, Descriptor.from_string, d) @@ -164,74 +177,53 @@ def test_descriptor_from_string_validation(self): keys = [ # # LIANA_MINISCRIPT_DESCRIPTOR "wsh(or_d(pk([55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*),and_v(v:pkh([3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*),older(6))))#x09nw3rv", - # # LIANA_TAPROOT_MINISCRIPT_DESCRIPTOR "tr([55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,and_v(v:pk([3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<4;2>/*),older(6)))#qjluv5ue", - # # LIANA_TAP_EXPANDING_MINISCRIPT_DESCRIPTOR "tr(xpub661MyMwAqRbcFHMDceyRcHhEfeDBXneBmbTnqujM6EumzeNcd8wrs3SHGzkETt7dDwqSCmDJx2rz6uKEddXRcYUWuAu6rkaj4L2QuVxqNUS/<0;1>/*,{and_v(v:multi_a(2,[55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<2;3>/*,[3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<2;3>/*,[d3a80c8b/48'/0'/0'/2']xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*),older(65535)),multi_a(2,[55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*)})#uyj29ygt", - # # UNSORTED_MULTISIG_DESCRIPTOR "wsh(multi(2,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*))", - # # UNRELATED_TAP_MINISCRIPT_DESCRIPTOR "tr(tpubD6NzVbkrYhZ4Y18xhod7E8V6Sy3YF36bge8HJb4ww1QgTrdkNvCEzcvUmFGQkTJA32gqr3j94iE8vsUzYpv8Pn29JezD9YiYnxgUREhN3QR/<0;1>/*,{and_v(v:multi_a(2,[1f280825/48'/1'/0'/2']tpubDEx7EkaqE8rG5NsCrijASmBjWiNv6teugndQCs4YN6JDS4hpJ3QtSC4ifPAcE7LQXtjRgB96trmEucoLbsiYYMuvLthymAhssZQpEPPb1pU/<2;3>/*,[e1efb2e7/48'/1'/0'/2']tpubDFhm1JYGdsR6Uv7SvXVd6JfjVkYimPDizEwwXRR9EhESpMhx3qL9nVjpfbtPRLzicWhYkMF4mn4AuZ4zYDjNMvuWSqugFBEJnYsMJurmbLM/<2;3>/*,[b32caab5/48'/1'/0'/2']tpubDEwY4xag4eQabW74PwS8BZb3aYy9mBzBffzBKqS74NjxzaDHodGGqfFLumwQGM5JYExNjs1mG3u8MaeEr94HNmxTaBPHERkoJXEcZ12aPdF/<0;1>/*),older(144)),multi_a(2,[1f280825/48'/1'/0'/2']tpubDEx7EkaqE8rG5NsCrijASmBjWiNv6teugndQCs4YN6JDS4hpJ3QtSC4ifPAcE7LQXtjRgB96trmEucoLbsiYYMuvLthymAhssZQpEPPb1pU/<0;1>/*,[e1efb2e7/48'/1'/0'/2']tpubDFhm1JYGdsR6Uv7SvXVd6JfjVkYimPDizEwwXRR9EhESpMhx3qL9nVjpfbtPRLzicWhYkMF4mn4AuZ4zYDjNMvuWSqugFBEJnYsMJurmbLM/<0;1>/*)})#u5clzmqy", - # # UNRELATED_MINISCRIPT_DESCRIPTOR "wsh(or_d(multi(2,[1f280825/48'/1'/0'/2']tpubDEx7EkaqE8rG5NsCrijASmBjWiNv6teugndQCs4YN6JDS4hpJ3QtSC4ifPAcE7LQXtjRgB96trmEucoLbsiYYMuvLthymAhssZQpEPPb1pU/<0;1>/*,[e1efb2e7/48'/1'/0'/2']tpubDFhm1JYGdsR6Uv7SvXVd6JfjVkYimPDizEwwXRR9EhESpMhx3qL9nVjpfbtPRLzicWhYkMF4mn4AuZ4zYDjNMvuWSqugFBEJnYsMJurmbLM/<0;1>/*),and_v(v:thresh(2,pkh([1f280825/48'/1'/0'/2']tpubDEx7EkaqE8rG5NsCrijASmBjWiNv6teugndQCs4YN6JDS4hpJ3QtSC4ifPAcE7LQXtjRgB96trmEucoLbsiYYMuvLthymAhssZQpEPPb1pU/<2;3>/*),a:pkh([e1efb2e7/48'/1'/0'/2']tpubDFhm1JYGdsR6Uv7SvXVd6JfjVkYimPDizEwwXRR9EhESpMhx3qL9nVjpfbtPRLzicWhYkMF4mn4AuZ4zYDjNMvuWSqugFBEJnYsMJurmbLM/<2;3>/*),a:pkh([b32caab5/48'/1'/0'/2']tpubDEwY4xag4eQabW74PwS8BZb3aYy9mBzBffzBKqS74NjxzaDHodGGqfFLumwQGM5JYExNjs1mG3u8MaeEr94HNmxTaBPHERkoJXEcZ12aPdF/<0;1>/*)),older(144))))#tfk3syfj", - # # UNAMBIGUOUS_MULTISIG_DESCRIPTOR "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1>/*,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/<0;1>/*))", - # # UNAMBIGUOUS_SINGLESIG_DESCRIPTOR "wpkh([55f8fc5d/84h/0h/0h]xpub6DPMTPxGMqdtzMwpqT1dDQaVdyaEppEm2qYSaJ7ANsuES7HkNzrXJst1Ed8D7NAnijUdgSDUFgph1oj5LKKAD5gyxWNhNP2AuDqaKYqzphA/<0;1>/*)", - # # MULTIPLE BRANCHES UNAMBIGUOUS_SINGLESIG_DESCRIPTOR "wpkh([55f8fc5d/84h/0h/0h]xpub6DPMTPxGMqdtzMwpqT1dDQaVdyaEppEm2qYSaJ7ANsuES7HkNzrXJst1Ed8D7NAnijUdgSDUFgph1oj5LKKAD5gyxWNhNP2AuDqaKYqzphA/<0;1;4;2;0;5>/*)", - # # UR_BYTES_MULTISIG_DESCRIPTOR "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/0/*,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/0/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/0/*))", - # # MULTIPLE BRANCHES UR_BYTES_MULTISIG_DESCRIPTOR "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/0/2/4/1/0/*,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/0/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/0/1/*))", - # # UR_OUTPUT_MULTISIG_DESCRIPTOR "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))", - # # MULTIPLE BRANCHES UR_OUTPUT_MULTISIG_DESCRIPTOR "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/2/1/0/2/1/1/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/1/1/*))", - # # BLUEWALLET_MULTISIG_DESCRIPTOR "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv))", - # # SPECTER_SINGLESIG_DESCRIPTOR "wpkh([55f8fc5d/84h/0h/0h]xpub6DPMTPxGMqdtzMwpqT1dDQaVdyaEppEm2qYSaJ7ANsuES7HkNzrXJst1Ed8D7NAnijUdgSDUFgph1oj5LKKAD5gyxWNhNP2AuDqaKYqzphA/0/*)", - # # SPECTER_MULTISIG_DESCRIPTOR "wsh(sortedmulti(2,[55f8fc5d/48h/0h/0h/2h]xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/0/*,[3e15470d/48h/0h/0h/2h]xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/0/*,[d3a80c8b/48h/0h/0h/2h]xpub6FKYY6y3oVi7ihSCszFKRSeZj5SzrfSsUFXhKqjMV4iigrLhxwMX3mrjioNyLTZ5iD3u4wU9S3tyzpJGxhd5geaXoQ68jGz2M6dfh2zJrUv/0/*))", - # # liana miniscript primary tapkey anytime, else 1of2 after 3 confirmations recovery taproot "tr([d63dc4a7/48'/1'/0'/2']tpubDEXCvh2aPYzMz2xfgsh9ZM6dQZxioYfCafUgw16keqschYbED4VeS46Qhr7EoonDHNr9dSsKPEGeRP5WRzDGdY3aazneR7wKmtDVNTf6qic/<0;1>/*,and_v(v:multi_a(1,[c98cbe58/48'/1'/0'/2']tpubDFXZ3rcRyvU6AvNrb4kRQFomJbtCTCyMX9jDJmfN5XfHLEAZq7a8h3CrYDZYtdexk6XWfT5DB8PYgySWA5GSdyWdzWwveQcbrzvVQw3u7bV/<0;1>/*,[9590b69a/48'/1'/0'/2']tpubDEgtrNHQ68KvQPABjV4Ah39MpUH6aniH8gbHKygJSwNwbsQpnzPJMcssdqjwPtNshjAj8nP35iZisEFchFdZtPG4rXi7FW35dsCtQSj93Qv/<0;1>/*),older(3)))#9550ke77", - # # liana miniscript wsh single primary key anytime, else a secondary key after 6 confirmations "wsh(or_d(pk([d63dc4a7/48'/1'/0'/2']tpubDEXCvh2aPYzMz2xfgsh9ZM6dQZxioYfCafUgw16keqschYbED4VeS46Qhr7EoonDHNr9dSsKPEGeRP5WRzDGdY3aazneR7wKmtDVNTf6qic/<0;1>/*),and_v(v:pkh([c98cbe58/48'/1'/0'/2']tpubDFXZ3rcRyvU6AvNrb4kRQFomJbtCTCyMX9jDJmfN5XfHLEAZq7a8h3CrYDZYtdexk6XWfT5DB8PYgySWA5GSdyWdzWwveQcbrzvVQw3u7bV/<0;1>/*),older(6))))#szdmyf2d", - # # liana expanding multisig taproot w/ NUMS tapkey and 2 taproot paths, primary 2of2 anytime else 2of3 after 36 confirmations. - "tr(tpubD6NzVbkrYhZ4X6BRkDMxFyZxfUCQdjpK27dNgqwDqsQ2PUbMmjjPPFxfcTJiGEjeNz2zLbZ1PRmgCAzXn4pE6tEuQPScXyUbuAgdcec6pMN/<0;1>/*,{and_v(v:multi_a(2,[07fd816d/48'/1'/0'/2']tpubDDvFWduSiwhW7hUbL1oMyUfcNgeSyZgHbooe1WjHyRaXYH3uUjm1xdxWXAGbQFn8QGScDg4b4a6WMGNiEAq2uQdmPDhDKPE5Dr8DX24mwd5/<2;3>/*,[da855a1f/48'/1'/0'/2']tpubDEHRt73d4guqR5BLGQud4XMW8vDCGHUj54qDTFtsdFstF6PAYx1oAy3jfKg1PffqLUWuSsXmnetKeTJFKfKLXeJR97yUuqvvojnoBcUDHg5/<2;3>/*,[cdef7cd9/48'/1'/0'/2']tpubDEzdWp7365AFAExeUsHiwRmkZN5it3sSAZsd6GKUXvUiJBytXnZrRKMAt9UgCkWB2mP3K9WujLuTjrRLBn51Y18pMVyg2v18un4ivqWSAk2/<0;1>/*),older(36)),multi_a(2,[07fd816d/48'/1'/0'/2']tpubDDvFWduSiwhW7hUbL1oMyUfcNgeSyZgHbooe1WjHyRaXYH3uUjm1xdxWXAGbQFn8QGScDg4b4a6WMGNiEAq2uQdmPDhDKPE5Dr8DX24mwd5/<0;1>/*,[da855a1f/48'/1'/0'/2']tpubDEHRt73d4guqR5BLGQud4XMW8vDCGHUj54qDTFtsdFstF6PAYx1oAy3jfKg1PffqLUWuSsXmnetKeTJFKfKLXeJR97yUuqvvojnoBcUDHg5/<0;1>/*)})#tvh3u2lu" + "tr(tpubD6NzVbkrYhZ4X6BRkDMxFyZxfUCQdjpK27dNgqwDqsQ2PUbMmjjPPFxfcTJiGEjeNz2zLbZ1PRmgCAzXn4pE6tEuQPScXyUbuAgdcec6pMN/<0;1>/*,{and_v(v:multi_a(2,[07fd816d/48'/1'/0'/2']tpubDDvFWduSiwhW7hUbL1oMyUfcNgeSyZgHbooe1WjHyRaXYH3uUjm1xdxWXAGbQFn8QGScDg4b4a6WMGNiEAq2uQdmPDhDKPE5Dr8DX24mwd5/<2;3>/*,[da855a1f/48'/1'/0'/2']tpubDEHRt73d4guqR5BLGQud4XMW8vDCGHUj54qDTFtsdFstF6PAYx1oAy3jfKg1PffqLUWuSsXmnetKeTJFKfKLXeJR97yUuqvvojnoBcUDHg5/<2;3>/*,[cdef7cd9/48'/1'/0'/2']tpubDEzdWp7365AFAExeUsHiwRmkZN5it3sSAZsd6GKUXvUiJBytXnZrRKMAt9UgCkWB2mP3K9WujLuTjrRLBn51Y18pMVyg2v18un4ivqWSAk2/<0;1>/*),older(36)),multi_a(2,[07fd816d/48'/1'/0'/2']tpubDDvFWduSiwhW7hUbL1oMyUfcNgeSyZgHbooe1WjHyRaXYH3uUjm1xdxWXAGbQFn8QGScDg4b4a6WMGNiEAq2uQdmPDhDKPE5Dr8DX24mwd5/<0;1>/*,[da855a1f/48'/1'/0'/2']tpubDEHRt73d4guqR5BLGQud4XMW8vDCGHUj54qDTFtsdFstF6PAYx1oAy3jfKg1PffqLUWuSsXmnetKeTJFKfKLXeJR97yUuqvvojnoBcUDHg5/<0;1>/*)})#tvh3u2lu", ] for d in keys: Descriptor.from_string(d) - # invalid keys = [ # # LIANA_MINISCRIPT_DESCRIPTOR "wsh(or_d(pk([55f8fc5d/48'/0'/0'/2']xpub6EKmKYGYc1WY6t9d3d9SksR8keSaPZbFa6tqsGiH4xVxx8d2YyxSX7WG6yXEX3CmG54dPCxaapDw1XsjwCmfoqP7tbsAeqMVfKvqSAu4ndy/<0;1;2>/*),and_v(v:pkh([3e15470d/48'/0'/0'/2']xpub6F2P6Pz5KLPgCc6pTBd2xxCunaSYWc8CdkL28W5z15pJrN3aCYY7mCUAkCMtqrgT2wdhAGgRnJxAkCCUpGKoXKxQ57yffEGmPwtYA3DEXwu/<0;1>/*),older(6))))#x09nw3rv", - # # UNRELATED_MINISCRIPT_DESCRIPTOR "wsh(or_d(multi(2,[1f280825/48'/1'/0'/2']tpubDEx7EkaqE8rG5NsCrijASmBjWiNv6teugndQCs4YN6JDS4hpJ3QtSC4ifPAcE7LQXtjRgB96trmEucoLbsiYYMuvLthymAhssZQpEPPb1pU/<0;1>/*,[e1efb2e7/48'/1'/0'/2']tpubDFhm1JYGdsR6Uv7SvXVd6JfjVkYimPDizEwwXRR9EhESpMhx3qL9nVjpfbtPRLzicWhYkMF4mn4AuZ4zYDjNMvuWSqugFBEJnYsMJurmbLM/<0;1>/*),and_v(v:thresh(2,pkh([1f280825/48'/1'/0'/2']tpubDEx7EkaqE8rG5NsCrijASmBjWiNv6teugndQCs4YN6JDS4hpJ3QtSC4ifPAcE7LQXtjRgB96trmEucoLbsiYYMuvLthymAhssZQpEPPb1pU/<2;3>/*),a:pkh([e1efb2e7/48'/1'/0'/2']tpubDFhm1JYGdsR6Uv7SvXVd6JfjVkYimPDizEwwXRR9EhESpMhx3qL9nVjpfbtPRLzicWhYkMF4mn4AuZ4zYDjNMvuWSqugFBEJnYsMJurmbLM/<2;3>/*),a:pkh([b32caab5/48'/1'/0'/2']tpubDEwY4xag4eQabW74PwS8BZb3aYy9mBzBffzBKqS74NjxzaDHodGGqfFLumwQGM5JYExNjs1mG3u8MaeEr94HNmxTaBPHERkoJXEcZ12aPdF/<0;1;2>/*)),older(144))))#tfk3syfj", - # # liana miniscript wsh single primary key anytime, else a secondary key after 6 confirmations "wsh(or_d(pk([d63dc4a7/48'/1'/0'/2']tpubDEXCvh2aPYzMz2xfgsh9ZM6dQZxioYfCafUgw16keqschYbED4VeS46Qhr7EoonDHNr9dSsKPEGeRP5WRzDGdY3aazneR7wKmtDVNTf6qic/<0;1>/*),and_v(v:pkh([c98cbe58/48'/1'/0'/2']tpubDFXZ3rcRyvU6AvNrb4kRQFomJbtCTCyMX9jDJmfN5XfHLEAZq7a8h3CrYDZYtdexk6XWfT5DB8PYgySWA5GSdyWdzWwveQcbrzvVQw3u7bV/*),older(6))))#szdmyf2d", ] @@ -344,36 +336,11 @@ def test_multisig(self): keys = tuple([ec.PrivateKey(bytes([i + 1] * 32)).to_public() for i in range(4)]) descriptors = [ # descriptor: str, is_basic_multisig: bool, is_sorted: bool - ( - "wsh(c:andor(multi(1,%s,%s),pk_k(%s),pk_k(%s)))" % keys, - False, - False, - 1 - ), - ( - "wsh(multi(2,%s,%s,%s,%s))" % keys, - True, - False, - 1 - ), - ( - "wsh(sortedmulti(2,%s,%s,%s,%s))" % keys, - True, - True, - 1 - ), - ( - "tr(%s,multi_a(2,%s,%s,%s))" % keys, - False, - False, - 1 - ), - ( - "tr(%s,sortedmulti_a(2,%s,%s,%s))" % keys, - False, - False, - 1 - ), + ("wsh(c:andor(multi(1,%s,%s),pk_k(%s),pk_k(%s)))" % keys, False, False, 1), + ("wsh(multi(2,%s,%s,%s,%s))" % keys, True, False, 1), + ("wsh(sortedmulti(2,%s,%s,%s,%s))" % keys, True, True, 1), + ("tr(%s,multi_a(2,%s,%s,%s))" % keys, False, False, 1), + ("tr(%s,sortedmulti_a(2,%s,%s,%s))" % keys, False, False, 1), ] for dstr, is_basic, is_sorted, n_branches in descriptors: d = Descriptor.from_string(dstr) diff --git a/tests/tests/test_taproot.py b/tests/tests/test_taproot.py index 3e3ad646..0ae1a55e 100644 --- a/tests/tests/test_taproot.py +++ b/tests/tests/test_taproot.py @@ -171,9 +171,9 @@ def test_sign_verify(self): # remove final scriptwitness to test signing with root inp.final_scriptwitness = None # populate derivation; Taproot signing expects X-only pubkeys - inp.bip32_derivations[ - PublicKey.from_xonly(prv.key.xonly()) - ] = DerivationPath(ROOT.my_fingerprint, [0, i]) + inp.bip32_derivations[PublicKey.from_xonly(prv.key.xonly())] = ( + DerivationPath(ROOT.my_fingerprint, [0, i]) + ) # test signing with root key counter = unsigned.sign_with(ROOT, SIGHASH.ALL) diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..44c99986 --- /dev/null +++ b/uv.lock @@ -0,0 +1,734 @@ +version = 1 +revision = 3 +requires-python = ">3.10, <=3.14.4" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "astroid" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } +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 = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "embit" +version = "0.8.0" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "isort" }, + { name = "poethepoet" }, + { name = "pre-commit" }, + { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "requests" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "isort", specifier = ">=5.13.0" }, + { name = "poethepoet", specifier = ">=0.45.0" }, + { name = "pre-commit", specifier = ">=3.7.0" }, + { name = "pylint", specifier = ">=3.3.0" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "requests", specifier = ">=2.33.1" }, + { name = "ruff", specifier = ">=0.9.0" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +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.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +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 = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +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 = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +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 = "pastel" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +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 = "poethepoet" +version = "0.45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pastel" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/28/d54e16913f591f01b2e1c4ab526ccbba47864d9bf10299904c823a760d25/poethepoet-0.45.0.tar.gz", hash = "sha256:cf2c2df4c185d33b619c2771de2b28c2b81c733f072226030c7fa4c273c040f8", size = 97150, upload-time = "2026-04-28T21:04:59.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/99/4aefb693b0f52783fab496492f4444a75137312c386eb7c3320f1b0a1602/poethepoet-0.45.0-py3-none-any.whl", hash = "sha256:8e25f6e834ecf25fe2ddca676a4e0207eeb2e19def0a8709fc5c7f18e86cd68c", size = 123920, upload-time = "2026-04-28T21:04:57.66Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pylint" +version = "4.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c", size = 1572474, upload-time = "2026-02-20T09:07:33.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { 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 = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, +]