├── tests ├── __init__.py ├── test_fast_withdraw.py ├── test_encryption.py ├── test_helpers_lnurlauth.py ├── test_models_from_dict.py ├── test_core.py ├── test_models.py └── test_types.py ├── lnurl ├── py.typed ├── exceptions.py ├── cli.py ├── __init__.py ├── helpers.py ├── core.py ├── models.py └── types.py ├── .gitignore ├── .editorconfig ├── Makefile ├── .github └── workflows │ ├── pypi.yaml │ ├── ruff.yml │ ├── mypy.yml │ ├── format.yml │ └── tests.yml ├── .pre-commit-config.yaml ├── LICENSE ├── pyproject.toml ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lnurl/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ._* 3 | 4 | __pycache__ 5 | *.py[cod] 6 | *$py.class 7 | .mypy_cache 8 | .vscode 9 | 10 | *.egg 11 | *.egg-info 12 | .coverage 13 | .pytest_cache 14 | .tox 15 | build 16 | dist 17 | htmlcov 18 | Pipfile.lock 19 | 20 | coverage.xml 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_size = 2 9 | indent_style = space 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.py] 15 | indent_size = 4 16 | indent_style = space 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: black ruff mypy test 2 | format: black ruff isort 3 | lint: ruff mypy 4 | check: checkblack checkruff 5 | 6 | black: 7 | poetry run black . 8 | 9 | ruff: 10 | poetry run ruff check . --fix 11 | 12 | checkruff: 13 | poetry run ruff check . 14 | 15 | checkblack: 16 | poetry run black --check . 17 | 18 | mypy: 19 | poetry run mypy . 20 | 21 | test: 22 | poetry run pytest tests --cov=lnurl --cov-report=xml 23 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "v[0-9]+.[0-9]+.[0-9]+" 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Build and publish to pypi 13 | uses: JRubics/poetry-publish@v1.15 14 | with: 15 | pypi_token: ${{ secrets.PYPI_API_KEY }} 16 | 17 | - name: Create github release 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | tag: ${{ github.ref_name }} 21 | run: | 22 | gh release create "$tag" --generate-notes 23 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: Ruff 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | ruff: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: "3.11" 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install ruff 20 | # Update output format to enable automatic inline annotations. 21 | - name: Run Ruff 22 | run: ruff check --output-format=github . 23 | -------------------------------------------------------------------------------- /lnurl/exceptions.py: -------------------------------------------------------------------------------- 1 | class LnurlException(Exception): 2 | """A LNURL error occurred.""" 3 | 4 | 5 | class LnAddressError(LnurlException): 6 | """An error ocurred processing LNURL address.""" 7 | 8 | 9 | class LnurlResponseException(LnurlException): 10 | """An error ocurred processing LNURL response.""" 11 | 12 | 13 | class InvalidLnurl(LnurlException, ValueError): 14 | """The LNURL provided was somehow invalid.""" 15 | 16 | 17 | class InvalidUrl(LnurlException, ValueError): 18 | """The URL is not properly formed.""" 19 | 20 | 21 | class InvalidLnurlPayMetadata(LnurlResponseException, ValueError): 22 | """The response `metadata` is not properly formed.""" 23 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | - id: check-docstring-first 10 | - id: check-json 11 | - id: debug-statements 12 | - id: mixed-line-ending 13 | - id: check-case-conflict 14 | - repo: https://github.com/psf/black 15 | rev: 23.7.0 16 | hooks: 17 | - id: black 18 | - repo: https://github.com/astral-sh/ruff-pre-commit 19 | rev: v0.0.283 20 | hooks: 21 | - id: ruff 22 | args: [ --fix, --exit-non-zero-on-fix ] 23 | -------------------------------------------------------------------------------- /.github/workflows/mypy.yml: -------------------------------------------------------------------------------- 1 | name: mypy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ["3.10", "3.12"] 13 | poetry-version: ["1.8.5"] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Poetry ${{ matrix.poetry-version }} 17 | uses: abatilo/actions-poetry@v2 18 | with: 19 | poetry-version: ${{ matrix.poetry-version }} 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | cache: 'poetry' 25 | - name: Install dependencies 26 | run: | 27 | poetry install 28 | - name: Run tests 29 | run: make mypy 30 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: black 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ["3.10", "3.12"] 13 | poetry-version: ["1.8.5"] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Poetry ${{ matrix.poetry-version }} 17 | uses: abatilo/actions-poetry@v2 18 | with: 19 | poetry-version: ${{ matrix.poetry-version }} 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | cache: "poetry" 25 | - name: Install dependencies 26 | run: | 27 | poetry install 28 | - name: Check black 29 | run: | 30 | make checkblack 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ["3.10", "3.12"] 13 | poetry-version: ["1.8.5"] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Poetry ${{ matrix.poetry-version }} 17 | uses: abatilo/actions-poetry@v2 18 | with: 19 | poetry-version: ${{ matrix.poetry-version }} 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | cache: "poetry" 25 | - name: Install dependencies 26 | run: | 27 | poetry install 28 | - name: Test with pytest 29 | run: | 30 | make test 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v3 33 | with: 34 | file: ./coverage.xml 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 Jon Forsberg 4 | Copyright (c) 2019 Eneko Illarramendi 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /tests/test_fast_withdraw.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import parse_obj_as 3 | 4 | from lnurl import encode 5 | from lnurl.models import LnurlWithdrawResponse 6 | 7 | url = "https://lnbits.com/withdraw/api/v1/lnurl" 8 | url2 = f"{url}?tag=withdrawRequest" 9 | url3 = f"{url2}&k1={16 * '0'}" 10 | url4 = f"{url3}&callback={url}&defaultDescription=default" 11 | url5 = f"{url4}&minWithdrawable=1000&maxWithdrawable=1000000" 12 | 13 | 14 | class TestFastWithdraw: 15 | @pytest.mark.parametrize( 16 | "url, expected", 17 | [ 18 | (url, False), 19 | (url2, False), 20 | (url3, False), 21 | (url4, False), 22 | (url5, True), 23 | ], 24 | ) 25 | def test_is_lnurl_fast_withdraw(self, url: str, expected: bool): 26 | lnurl = encode(url) 27 | assert lnurl.is_fast_withdraw == expected 28 | 29 | def test_set_lnurl_fast_withdraw(self): 30 | response = parse_obj_as( 31 | LnurlWithdrawResponse, 32 | { 33 | "tag": "withdrawRequest", 34 | "k1": "0" * 16, 35 | "minWithdrawable": 1000, 36 | "maxWithdrawable": 1000000, 37 | "defaultDescription": "default", 38 | "callback": url, 39 | }, 40 | ) 41 | lnurl = encode(f"{url}?{response.fast_withdraw_query}") 42 | assert lnurl.is_fast_withdraw is True 43 | -------------------------------------------------------------------------------- /tests/test_encryption.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode, b64encode 2 | 3 | import pytest 4 | from Cryptodome import Random 5 | 6 | from lnurl.helpers import aes_decrypt, aes_encrypt 7 | 8 | 9 | class TestAesEncryption: 10 | @pytest.mark.parametrize("plaintext", ["dni was here", "short", "A" * 4096]) 11 | def test_encrypt_decrypt(self, plaintext): 12 | preimage = Random.get_random_bytes(32) 13 | ciphertext, iv = aes_encrypt(preimage, plaintext) 14 | assert ciphertext != plaintext 15 | assert len(b64decode(ciphertext)) % 16 == 0 16 | assert len(iv) == 24 17 | assert len(b64decode(iv)) == 16 18 | assert aes_decrypt(preimage, ciphertext, iv) == plaintext 19 | with pytest.raises(ValueError): 20 | assert aes_decrypt(bytes(32), ciphertext, iv) == plaintext 21 | with pytest.raises(ValueError): 22 | assert aes_decrypt(preimage, ciphertext, b64encode(bytes(16)).decode()) == plaintext 23 | with pytest.raises(ValueError): 24 | assert aes_decrypt(preimage, b64encode(bytes(32)).decode(), iv) == plaintext 25 | 26 | def test_encrypt_empty(self): 27 | with pytest.raises(ValueError): 28 | _ = aes_encrypt(b64encode(bytes(32)), "") 29 | 30 | def test_encrypt_fails_too_small_key(self): 31 | with pytest.raises(ValueError): 32 | _ = aes_encrypt(b64encode(bytes(33)), "dni was here") 33 | 34 | def test_encrypt_fails_too_big_key(self): 35 | with pytest.raises(ValueError): 36 | _ = aes_encrypt(b64encode(bytes(31)), "dni was here") 37 | 38 | # TODO: interesting, why? 39 | def test_encrypt_fails_for_icons(self): 40 | icons = "lightning icons: ⚡⚡" 41 | with pytest.raises(Exception): 42 | _ = aes_encrypt(b64encode(bytes(32)), icons) 43 | -------------------------------------------------------------------------------- /lnurl/cli.py: -------------------------------------------------------------------------------- 1 | """lnurl CLI""" 2 | 3 | import asyncio 4 | import sys 5 | 6 | import click 7 | 8 | from .core import encode as encode_lnurl 9 | from .core import execute as execute_lnurl 10 | from .core import handle as handle_lnurl 11 | from .types import Lnurl 12 | 13 | # disable tracebacks on exceptions 14 | sys.tracebacklimit = 0 15 | 16 | 17 | @click.group() 18 | def command_group(): 19 | """ 20 | Python CLI for LNURL 21 | decode and encode lnurls""" 22 | 23 | 24 | @click.command() 25 | @click.argument("url", type=str) 26 | def encode(url): 27 | """ 28 | encode a URL 29 | """ 30 | encoded = encode_lnurl(url) 31 | click.echo(encoded.bech32) 32 | 33 | 34 | @click.command() 35 | @click.argument("lnurl", type=str) 36 | def decode(lnurl): 37 | """ 38 | decode a LNURL 39 | """ 40 | decoded = Lnurl(lnurl) 41 | click.echo(decoded.url) 42 | 43 | 44 | @click.command() 45 | @click.argument("lnurl", type=str) 46 | def handle(lnurl): 47 | """ 48 | handle a LNURL 49 | """ 50 | decoded = asyncio.run(handle_lnurl(lnurl)) 51 | click.echo(decoded.json()) 52 | 53 | 54 | @click.command() 55 | @click.argument("lnurl", type=str) 56 | @click.argument("msat_or_login", type=str, required=False) 57 | def execute(lnurl, msat_or_login): 58 | """ 59 | execute a LNURL request 60 | """ 61 | if not msat_or_login: 62 | raise ValueError("You must provide either an amount_msat or a login_id.") 63 | res = asyncio.run(execute_lnurl(lnurl, msat_or_login)) 64 | click.echo(res.json()) 65 | 66 | 67 | def main(): 68 | """main function""" 69 | command_group.add_command(encode) 70 | command_group.add_command(decode) 71 | command_group.add_command(handle) 72 | command_group.add_command(execute) 73 | command_group() 74 | 75 | 76 | if __name__ == "__main__": 77 | main() 78 | -------------------------------------------------------------------------------- /tests/test_helpers_lnurlauth.py: -------------------------------------------------------------------------------- 1 | from lnurl import ( 2 | lnurlauth_derive_linking_key, 3 | lnurlauth_derive_linking_key_sign_message, 4 | lnurlauth_derive_path, 5 | lnurlauth_master_key_from_seed, 6 | lnurlauth_message_to_sign, 7 | lnurlauth_signature, 8 | lnurlauth_verify, 9 | ) 10 | 11 | # taken from LUD-04 signature check example 12 | k1 = "e2af6254a8df433264fa23f67eb8188635d15ce883e8fc020989d5f82ae6f11e" 13 | key = "02c3b844b8104f0c1b15c507774c9ba7fc609f58f343b9b149122e944dd20c9362" 14 | sig = ( 15 | "304402203767faf494f110b139293d9bab3c50e07b3bf33c463d4aa767256cd09132dc510" 16 | "2205821f8efacdb5c595b92ada255876d9201e126e2f31a140d44561cc1f7e9e43d" 17 | ) 18 | 19 | # LUD-05 derive path example 20 | domain_name = "site.com" 21 | hashing_private_key = "7d417a6a5e9a6a4a879aeaba11a11838764c8fa2b959c242d43dea682b3e409b" 22 | path_suffix = "m/138'/1588488367/2659270754/38110259/4136336762" 23 | 24 | # LUD-13 phrase sha256 25 | phrase = "4b8dac0e71a99c61d2c197e6932fab3ae3cf7900fc19076864aa11e80d83b4d9" 26 | 27 | 28 | class TestHelpersLnurlauth: 29 | 30 | def test_verify(self): 31 | assert lnurlauth_verify(k1, key, sig) is True 32 | 33 | def test_verify_invalid(self): 34 | k1 = "0" * 32 35 | key = "0" * 33 36 | sig = "0" * 64 37 | assert lnurlauth_verify(k1, key, sig) is False 38 | 39 | def test_signature(self): 40 | linking_key, _ = lnurlauth_derive_linking_key(key, domain_name) 41 | _key, _sig = lnurlauth_signature(k1, linking_key) 42 | assert lnurlauth_verify(k1, _key, _sig) is True 43 | # invalid signature 44 | assert lnurlauth_verify(k1, key, _sig) is False 45 | 46 | def test_derive_path(self): 47 | _path = lnurlauth_derive_path(bytes.fromhex(hashing_private_key), domain_name) 48 | assert _path == path_suffix 49 | 50 | def test_master_key_from_seed(self): 51 | _master = lnurlauth_master_key_from_seed(key) 52 | assert _master 53 | assert _master.privkey 54 | 55 | def test_phrase_sha256(self): 56 | assert lnurlauth_message_to_sign().hex() == phrase 57 | 58 | def test_linking_key_signmessage(self): 59 | _ = lnurlauth_derive_linking_key_sign_message(domain_name, b"0" * 32) 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "lnurl" 3 | version = "0.8.3" 4 | description = "LNURL implementation for Python." 5 | authors = ["Alan Bits "] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [ 9 | {include = "lnurl"}, 10 | {include = "lnurl/py.typed"}, 11 | ] 12 | 13 | [tool.poetry.scripts] 14 | lnurl = "lnurl.cli:main" 15 | 16 | [tool.poetry.dependencies] 17 | python = ">=3.10" 18 | pydantic = "^1" 19 | bech32 = "*" 20 | ecdsa = "*" 21 | bolt11 = "*" 22 | httpx = "*" 23 | pycryptodomex = "^3.21.0" 24 | bip32 = "^4.0" 25 | 26 | [tool.poetry.group.dev.dependencies] 27 | black = "^24.3.0" 28 | pytest = "^7.4.0" 29 | pytest-cov = "^4.1.0" 30 | types-requests = "^2.31.0.2" 31 | mypy = "^1.17.1" 32 | ruff = "^0.12.3" 33 | pre-commit = "^3.3.3" 34 | pytest-asyncio = "^0.23.6" 35 | 36 | [build-system] 37 | requires = ["poetry-core"] 38 | build-backend = "poetry.core.masonry.api" 39 | 40 | [tool.mypy] 41 | ignore_missing_imports = "True" 42 | files = "lnurl" 43 | plugins = "pydantic.mypy" 44 | 45 | [tool.pydantic-mypy] 46 | init_forbid_extra = true 47 | init_typed = true 48 | warn_required_dynamic_aliases = true 49 | warn_untyped_fields = true 50 | 51 | [tool.pytest.ini_options] 52 | testpaths = [ 53 | "tests" 54 | ] 55 | 56 | [tool.black] 57 | line-length = 120 58 | preview = true 59 | 60 | [tool.ruff] 61 | # Same as Black. but black has a 10% overflow rule 62 | line-length = 120 63 | 64 | # Exclude a variety of commonly ignored directories. 65 | exclude = [ 66 | ".bzr", 67 | ".direnv", 68 | ".eggs", 69 | ".git", 70 | ".git-rewrite", 71 | ".hg", 72 | ".mypy_cache", 73 | ".nox", 74 | ".pants.d", 75 | ".pytype", 76 | ".ruff_cache", 77 | ".svn", 78 | ".tox", 79 | ".venv", 80 | "__pypackages__", 81 | "_build", 82 | "buck-out", 83 | "build", 84 | "dist", 85 | "node_modules", 86 | "venv", 87 | ] 88 | 89 | [tool.ruff.lint] 90 | # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. 91 | # (`I`) means isorting 92 | select = ["E", "F", "I"] 93 | ignore = [] 94 | 95 | # Allow autofix for all enabled rules (when `--fix`) is provided. 96 | fixable = ["ALL"] 97 | unfixable = [] 98 | 99 | # Allow unused variables when underscore-prefixed. 100 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 101 | 102 | [tool.ruff.lint.mccabe] 103 | # Unlike Flake8, default to a complexity level of 10. 104 | max-complexity = 10 105 | -------------------------------------------------------------------------------- /lnurl/__init__.py: -------------------------------------------------------------------------------- 1 | # backward compatibility, MilliSatoshi is now imported from bolt11 2 | from bolt11 import MilliSatoshi 3 | 4 | from .core import decode, encode, execute, execute_login, execute_pay_request, execute_withdraw, get, handle 5 | from .exceptions import ( 6 | InvalidLnurl, 7 | InvalidLnurlPayMetadata, 8 | InvalidUrl, 9 | LnAddressError, 10 | LnurlException, 11 | LnurlResponseException, 12 | ) 13 | from .helpers import ( 14 | LUD13_PHRASE, 15 | aes_decrypt, 16 | aes_encrypt, 17 | lnurlauth_derive_linking_key, 18 | lnurlauth_derive_linking_key_sign_message, 19 | lnurlauth_derive_path, 20 | lnurlauth_master_key_from_seed, 21 | lnurlauth_message_to_sign, 22 | lnurlauth_signature, 23 | lnurlauth_verify, 24 | url_decode, 25 | url_encode, 26 | ) 27 | from .models import ( 28 | AesAction, 29 | LnurlAuthResponse, 30 | LnurlChannelResponse, 31 | LnurlErrorResponse, 32 | LnurlHostedChannelResponse, 33 | LnurlPayActionResponse, 34 | LnurlPayerData, 35 | LnurlPayerDataAuth, 36 | LnurlPayResponse, 37 | LnurlPayResponsePayerData, 38 | LnurlPayResponsePayerDataExtra, 39 | LnurlPayResponsePayerDataOption, 40 | LnurlPayResponsePayerDataOptionAuth, 41 | LnurlPayRouteHop, 42 | LnurlPaySuccessAction, 43 | LnurlPayVerifyResponse, 44 | LnurlResponse, 45 | LnurlResponseModel, 46 | LnurlSuccessResponse, 47 | LnurlWithdrawResponse, 48 | MessageAction, 49 | UrlAction, 50 | ) 51 | from .types import ( 52 | Bech32, 53 | CallbackUrl, 54 | CiphertextBase64, 55 | InitializationVectorBase64, 56 | LightningInvoice, 57 | LightningNodeUri, 58 | LnAddress, 59 | Lnurl, 60 | LnurlAuthActions, 61 | LnurlPayMetadata, 62 | LnurlPaySuccessActionTag, 63 | LnurlResponseTag, 64 | LnurlStatus, 65 | Max144Str, 66 | Url, 67 | ) 68 | 69 | __all__ = [ 70 | "aes_decrypt", 71 | "aes_encrypt", 72 | "url_encode", 73 | "url_decode", 74 | "decode", 75 | "encode", 76 | "execute", 77 | "execute_login", 78 | "execute_pay_request", 79 | "execute_withdraw", 80 | "get", 81 | "handle", 82 | "Lnurl", 83 | "LnurlAuthActions", 84 | "LnurlAuthResponse", 85 | "LnurlChannelResponse", 86 | "LnurlErrorResponse", 87 | "LnurlHostedChannelResponse", 88 | "LnurlPayActionResponse", 89 | "LnurlPayResponse", 90 | "LnurlPayMetadata", 91 | "LnurlPaySuccessAction", 92 | "LnurlPaySuccessActionTag", 93 | "LnurlPayVerifyResponse", 94 | "LnurlResponse", 95 | "LnurlResponseModel", 96 | "LnurlResponseTag", 97 | "LnurlStatus", 98 | "LnurlSuccessResponse", 99 | "LnurlWithdrawResponse", 100 | "MilliSatoshi", 101 | "CallbackUrl", 102 | "Url", 103 | "LightningNodeUri", 104 | "LnAddress", 105 | "LightningInvoice", 106 | "Bech32", 107 | "LnurlPayRouteHop", 108 | "MessageAction", 109 | "UrlAction", 110 | "AesAction", 111 | "InitializationVectorBase64", 112 | "CiphertextBase64", 113 | "lnurlauth_signature", 114 | "lnurlauth_verify", 115 | "lnurlauth_derive_linking_key", 116 | "lnurlauth_master_key_from_seed", 117 | "lnurlauth_derive_path", 118 | "lnurlauth_message_to_sign", 119 | "lnurlauth_derive_linking_key_sign_message", 120 | "LUD13_PHRASE", 121 | "LnurlException", 122 | "LnAddressError", 123 | "InvalidLnurl", 124 | "InvalidLnurlPayMetadata", 125 | "InvalidUrl", 126 | "LnurlResponseException", 127 | "LnurlPayerData", 128 | "LnurlPayerDataAuth", 129 | "LnurlPayResponsePayerData", 130 | "LnurlPayResponsePayerDataOption", 131 | "LnurlPayResponsePayerDataOptionAuth", 132 | "LnurlPayResponsePayerDataExtra", 133 | "Max144Str", 134 | ] 135 | -------------------------------------------------------------------------------- /tests/test_models_from_dict.py: -------------------------------------------------------------------------------- 1 | import json 2 | from base64 import b64encode 3 | 4 | import pytest 5 | 6 | from lnurl import ( 7 | AesAction, 8 | LnurlErrorResponse, 9 | LnurlPayActionResponse, 10 | LnurlPayResponse, 11 | LnurlPaySuccessAction, 12 | LnurlPaySuccessActionTag, 13 | LnurlResponse, 14 | LnurlSuccessResponse, 15 | LnurlWithdrawResponse, 16 | ) 17 | from lnurl.exceptions import LnurlResponseException 18 | 19 | 20 | class TestLnurlResponse: 21 | pay_res = json.loads( 22 | r'{"tag":"payRequest","metadata":"[[\"text/plain\",\"lorem ipsum blah blah\"]]",' 23 | '"callback":"https://lnurl.bigsun.xyz/lnurl-pay/callback/","maxSendable":300980,' 24 | '"minSendable":100980}' 25 | ) # noqa 26 | pay_res_invalid = json.loads(r'{"tag":"payRequest","metadata":"[\"text\"\"plain\"]"}') 27 | withdraw_res = json.loads( 28 | '{"tag":"withdrawRequest","k1":"c67a8aa61f7c6cd457058916356ca80f5bfd00fa78ac2c1b3157391c2e9787de",' 29 | '"callback":"https://lnurl.bigsun.xyz/lnurl-withdraw/callback/?param1=1¶m2=2",' 30 | '"maxWithdrawable":478980,"minWithdrawable":478980,"defaultDescription":"sample withdraw"}' 31 | ) # noqa 32 | pay_res_action_aes = { 33 | "pr": ( 34 | "lnbc1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqq" 35 | "syqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8q" 36 | "un0dfjkxaq9qrsgq357wnc5r2ueh7ck6q93dj32dlqnls087fxdwk8qakdyafkq3yap9us6v52vjjsrvywa6rt52c" 37 | "m9r9zqt8r2t7mlcwspyetp5h2tztugp9lfyql" 38 | ), 39 | "routes": [], 40 | "successAction": { 41 | "tag": "aes", 42 | "description": "your will receive a secret message", 43 | "iv": b64encode(bytes(16)), 44 | "ciphertext": b64encode(bytes(32)), 45 | }, 46 | } 47 | 48 | def test_error(self): 49 | res = LnurlResponse.from_dict({"status": "error", "reason": "error details..."}) 50 | assert isinstance(res, LnurlErrorResponse) 51 | assert not res.ok 52 | assert res.error_msg == "error details..." 53 | 54 | def test_success(self): 55 | res = LnurlResponse.from_dict({"status": "OK"}) 56 | assert isinstance(res, LnurlSuccessResponse) 57 | assert res.ok 58 | 59 | def test_unknown(self): 60 | with pytest.raises(LnurlResponseException): 61 | LnurlResponse.from_dict({"status": "unknown"}) 62 | 63 | def test_pay(self): 64 | res = LnurlResponse.from_dict(self.pay_res) 65 | assert isinstance(res, LnurlPayResponse) 66 | assert res.ok 67 | assert res.max_sats == 300 68 | assert res.min_sats == 101 69 | assert res.metadata == '[["text/plain","lorem ipsum blah blah"]]' 70 | assert res.metadata.list() == [("text/plain", "lorem ipsum blah blah")] 71 | assert not res.metadata.images 72 | assert res.metadata.text == "lorem ipsum blah blah" 73 | assert res.metadata.h == "d824d0ea606c5a9665279c31cf185528a8df2875ea93f1f75e501e354b33e90a" 74 | 75 | def test_pay_invalid_metadata(self): 76 | with pytest.raises(LnurlResponseException): 77 | LnurlResponse.from_dict(self.pay_res_invalid) 78 | 79 | # LUD-10 80 | def test_pay_action_aes(self): 81 | res = LnurlResponse.from_dict(self.pay_res_action_aes) 82 | assert isinstance(res, LnurlPayActionResponse) 83 | assert isinstance(res.successAction, AesAction) 84 | assert isinstance(res.successAction, LnurlPaySuccessAction) 85 | assert res.ok 86 | assert res.successAction 87 | assert res.successAction.tag == LnurlPaySuccessActionTag.aes 88 | assert res.successAction.description == "your will receive a secret message" 89 | assert len(res.successAction.iv) == 24 90 | assert len(res.successAction.ciphertext) == 44 91 | 92 | def test_withdraw(self): 93 | res = LnurlResponse.from_dict(self.withdraw_res) 94 | assert isinstance(res, LnurlWithdrawResponse) 95 | assert res.ok 96 | assert res.maxWithdrawable == 478980 97 | assert res.max_sats == 478 98 | assert res.min_sats == 479 99 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | 4 | from lnurl.core import decode, encode, execute_login, execute_pay_request, get, handle 5 | from lnurl.exceptions import InvalidLnurl, InvalidUrl, LnurlResponseException 6 | from lnurl.models import ( 7 | LnurlAuthResponse, 8 | LnurlPayActionResponse, 9 | LnurlPayResponse, 10 | LnurlPaySuccessAction, 11 | LnurlSuccessResponse, 12 | LnurlWithdrawResponse, 13 | ) 14 | from lnurl.types import Lnurl, Url 15 | 16 | 17 | class TestDecode: 18 | @pytest.mark.parametrize( 19 | "bech32, url", 20 | [ 21 | ( 22 | ( 23 | "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWE3JX56NXCFK89JN2V3K" 24 | "XUCRSVTY8YMXGCMYXV6RQD3EXDSKVCTZV5CRGCN9XA3RQCMRVSCNWWRYVCYAE0UU" 25 | ), 26 | "https://service.io/?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df", 27 | ) 28 | ], 29 | ) 30 | def test_decode(self, bech32, url): 31 | decoded = decode(bech32) 32 | assert isinstance(decoded, Lnurl) 33 | decoded_url = decoded.url 34 | assert isinstance(decoded_url, Url) 35 | assert decoded_url == str(decoded_url) == url 36 | assert decoded_url.host == "service.io" 37 | 38 | @pytest.mark.parametrize( 39 | "bech32", 40 | [ 41 | "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", 42 | "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", 43 | "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", 44 | ], 45 | ) 46 | def test_decode_nolnurl(self, bech32): 47 | with pytest.raises(InvalidLnurl): 48 | decode(bech32) 49 | 50 | 51 | class TestEncode: 52 | @pytest.mark.parametrize( 53 | "bech32, url", 54 | [ 55 | ( 56 | ( 57 | "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWE3JX56NXCFK89JN2V3K" 58 | "XUCRSVTY8YMXGCMYXV6RQD3EXDSKVCTZV5CRGCN9XA3RQCMRVSCNWWRYVCYAE0UU" 59 | ), 60 | "https://service.io/?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df", 61 | ) 62 | ], 63 | ) 64 | def test_encode(self, bech32, url): 65 | lnurl = encode(url) 66 | assert isinstance(lnurl, Lnurl) 67 | assert lnurl.bech32 == bech32 68 | assert lnurl.url.host == "service.io" 69 | 70 | @pytest.mark.parametrize("url", ["http://service.io/"]) 71 | def test_encode_nohttps(self, url): 72 | with pytest.raises(InvalidUrl): 73 | encode(url) 74 | 75 | 76 | class TestHandle: 77 | """Responses from the LNURL: https://demo.lnbits.com/""" 78 | 79 | @pytest.mark.xfail(reason="demo.lnbits.com is down") 80 | @pytest.mark.asyncio 81 | @pytest.mark.parametrize( 82 | "bech32", 83 | [ 84 | "LNURL1DP68GURN8GHJ7MR9VAJKUEPWD3HXY6T5WVHXXMMD9AMKJARGV3EXZAE0V9CXJTMKXYH" 85 | "KCMN4WFKZ7MJT2C6X2NRK0PDRYJNGWVU9WDN2G4V8XK2VSZA2RC" 86 | ], 87 | ) 88 | async def test_handle_withdraw(self, bech32): 89 | res = await handle(bech32) 90 | assert isinstance(res, LnurlWithdrawResponse) 91 | assert res.tag == "withdrawRequest" 92 | assert res.callback.host == "demo.lnbits.com" 93 | assert res.default_description == "sample withdraw" 94 | assert res.max_withdrawable >= res.min_withdrawable 95 | 96 | @pytest.mark.asyncio 97 | @pytest.mark.parametrize("bech32", ["BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"]) 98 | async def test_handle_nolnurl(self, bech32): 99 | with pytest.raises(InvalidLnurl): 100 | await handle(bech32) 101 | 102 | @pytest.mark.asyncio 103 | @pytest.mark.parametrize("url", ["https://lnurl.thisshouldfail.io/"]) 104 | async def test_get_requests_error(self, url): 105 | with pytest.raises(LnurlResponseException): 106 | await get(url) 107 | 108 | 109 | class TestPayFlow: 110 | """Full LNURL-pay flow interacting with https://demo.lnbits.com/""" 111 | 112 | @pytest.mark.xfail(reason="demo.lnbits.com is down") 113 | @pytest.mark.asyncio 114 | @pytest.mark.parametrize( 115 | "bech32, amount", 116 | [ 117 | ( 118 | "LNURL1DP68GURN8GHJ7MR9VAJKUEPWD3HXY6T5WVHXXMMD9AKXUATJD3CZ7JN9F4EHQJQC25ZZY", 119 | "1000", 120 | ), 121 | ("donate@demo.lnbits.com", "100000"), 122 | ], 123 | ) 124 | async def test_pay_flow(self, bech32: str, amount: str): 125 | res = await handle(bech32) 126 | assert isinstance(res, LnurlPayResponse) 127 | assert res.tag == "payRequest" 128 | assert res.callback.host == "demo.lnbits.com" 129 | assert len(res.metadata.list()) >= 1 130 | assert res.metadata.text != "" 131 | 132 | res2 = await execute_pay_request(res, amount) 133 | assert isinstance(res2, LnurlPayActionResponse) 134 | assert res2.success_action is None or isinstance(res2.success_action, LnurlPaySuccessAction) 135 | 136 | 137 | class TestLoginFlow: 138 | """Full LNURL-login flow interacting with https://lnmarkets.com/""" 139 | 140 | @pytest.mark.asyncio 141 | @pytest.mark.xfail(reason="need online lnurl auth server to test this flow") 142 | @pytest.mark.parametrize( 143 | "url", 144 | [ 145 | "https://api.lnmarkets.com/trpc/lnurl.auth.new", 146 | ], 147 | ) 148 | async def test_login_flow(self, url: str): 149 | async with httpx.AsyncClient() as client: 150 | init = await client.get(url) 151 | init.raise_for_status() 152 | bech32 = init.json()["result"]["data"]["json"]["lnurl"] 153 | 154 | res = await handle(bech32) 155 | assert isinstance(res, LnurlAuthResponse) 156 | assert res.tag == "login" 157 | assert res.callback.host == "api.lnmarkets.com" 158 | 159 | res2 = await execute_login(res, "my-secret") 160 | assert isinstance(res2, LnurlSuccessResponse) 161 | -------------------------------------------------------------------------------- /lnurl/helpers.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | from base64 import b64decode, b64encode 3 | from hashlib import sha256 4 | from typing import List, Optional, Set, Tuple 5 | 6 | from bech32 import bech32_decode, bech32_encode, convertbits 7 | from bip32 import BIP32 8 | from Cryptodome import Random 9 | from Cryptodome.Cipher import AES 10 | from ecdsa import SECP256k1, SigningKey, VerifyingKey 11 | from ecdsa.util import sigdecode_der, sigencode_der 12 | 13 | from .exceptions import InvalidLnurl, InvalidUrl 14 | 15 | LUD13_PHRASE = ( 16 | "DO NOT EVER SIGN THIS TEXT WITH YOUR PRIVATE KEYS! IT IS ONLY USED " 17 | "FOR DERIVATION OF LNURL-AUTH HASHING-KEY, DISCLOSING ITS SIGNATURE " 18 | "WILL COMPROMISE YOUR LNURL-AUTH IDENTITY AND MAY LEAD TO LOSS OF FUNDS!" 19 | ) 20 | 21 | 22 | def aes_decrypt(preimage: bytes, ciphertext_base64: str, iv_base64: str) -> str: 23 | """ 24 | Decrypt a message using AES-CBC. LUD-10 25 | LUD-10, used in PayRequest success actions. 26 | """ 27 | if len(preimage) != 32: 28 | raise ValueError("AES key must be 32 bytes long") 29 | if len(iv_base64) != 24: 30 | raise ValueError("IV must be 24 bytes long") 31 | cipher = AES.new(preimage, AES.MODE_CBC, b64decode(iv_base64)) 32 | decrypted = cipher.decrypt(b64decode(ciphertext_base64)) 33 | size = len(decrypted) 34 | pad = decrypted[size - 1] 35 | if (0 > pad > 16) or (pad > 1 and decrypted[size - 2] != pad): 36 | raise ValueError("Decryption failed. Error with padding.") 37 | decrypted = decrypted[: size - pad] 38 | if len(decrypted) == 0: 39 | raise ValueError("Decryption failed. Empty message.") 40 | try: 41 | return decrypted.decode("utf-8") 42 | except UnicodeDecodeError as exc: 43 | raise ValueError("Decryption failed. UnicodeDecodeError") from exc 44 | 45 | 46 | def aes_encrypt(preimage: bytes, message: str) -> tuple[str, str]: 47 | """ 48 | Encrypt a message using AES-CBC with a random IV. 49 | LUD-10, used in PayRequest success actions. 50 | """ 51 | if len(preimage) != 32: 52 | raise ValueError("AES key must be 32 bytes long") 53 | if len(message) == 0: 54 | raise ValueError("Message must not be empty") 55 | iv = Random.get_random_bytes(16) 56 | cipher = AES.new(preimage, AES.MODE_CBC, iv) 57 | pad = 16 - len(message) % 16 58 | message += chr(pad) * pad 59 | ciphertext = cipher.encrypt(message.encode("utf-8")) 60 | return b64encode(ciphertext).decode(), b64encode(iv).decode("utf-8") 61 | 62 | 63 | # LUD-04: auth base spec. 64 | def lnurlauth_signature(k1: str, linking_key: bytes) -> tuple[str, str]: 65 | """ 66 | Sign a k1 with a linking_key from (bip32 or signMessage) and a domain. 67 | 68 | Obtain the linking_key from lnurlauth_derive_linking_key or lnurlauth_derive_linking_key_sign_message. 69 | """ 70 | auth_key = SigningKey.from_string(linking_key, curve=SECP256k1, hashfunc=sha256) 71 | sig = auth_key.sign_digest_deterministic(bytes.fromhex(k1), sigencode=sigencode_der) 72 | if not auth_key.verifying_key: 73 | raise ValueError("LNURLauth verifying_key does not exist") 74 | key = auth_key.verifying_key.to_string("compressed") 75 | return key.hex(), sig.hex() 76 | 77 | 78 | def lnurlauth_verify(k1: str, key: str, sig: str) -> bool: 79 | """ 80 | Verify a k1 with a key and a signature. 81 | """ 82 | try: 83 | verifying_key = VerifyingKey.from_string(bytes.fromhex(key), hashfunc=sha256, curve=SECP256k1) 84 | verifying_key.verify_digest(bytes.fromhex(sig), bytes.fromhex(k1), sigdecode=sigdecode_der) 85 | return True 86 | except Exception as exc: 87 | print(exc) 88 | return False 89 | 90 | 91 | # LUD-05: BIP32-based seed generation for auth protocol. 92 | def lnurlauth_derive_linking_key(seed: str, domain: str) -> tuple[bytes, bytes]: 93 | """ 94 | Derive a key from a masterkey. 95 | RETURN (linking_key, linking_key_pub) in hex 96 | """ 97 | master_key = lnurlauth_master_key_from_seed(seed) 98 | hashing_key = BIP32.get_privkey_from_path(master_key, "m/138'/0") 99 | _path_suffix = lnurlauth_derive_path(hashing_key, domain) 100 | linking_key = BIP32.get_privkey_from_path(master_key, _path_suffix) 101 | linking_key_pub = BIP32.get_pubkey_from_path(master_key, _path_suffix) 102 | return linking_key, linking_key_pub 103 | 104 | 105 | def lnurlauth_master_key_from_seed(seed: str) -> BIP32: 106 | """ 107 | Derive a masterkey from a seed. 108 | RETURN (linking_key, linking_key_pub) in hex 109 | """ 110 | master_key = BIP32.from_seed(bytes.fromhex(seed)) 111 | assert master_key.privkey 112 | return master_key 113 | 114 | 115 | def lnurlauth_derive_path(hashing_private_key: bytes, domain_name: str) -> str: 116 | """ 117 | Derive a path suffix from a hashing_key. 118 | 119 | Take the first 16 bytes of the hash turn it into 4 longs and make a new derivation path with it. 120 | m/138'//// 121 | """ 122 | derivation_material = hmac.digest(hashing_private_key, domain_name.encode(), "sha256") 123 | _path_suffix_longs = [int.from_bytes(derivation_material[i : i + 4], "big") for i in range(0, 16, 4)] 124 | _path_suffix = "m/138'/" + "/".join(str(i) for i in _path_suffix_longs) 125 | return _path_suffix 126 | 127 | 128 | # LUD-13: signMessage-based seed generation for auth protocol. 129 | def lnurlauth_message_to_sign() -> bytes: 130 | """ 131 | Generate a message to sign for signMessage. 132 | """ 133 | return sha256(LUD13_PHRASE.encode()).digest() 134 | 135 | 136 | def lnurlauth_derive_linking_key_sign_message(domain: str, sig: bytes) -> tuple[bytes, bytes]: 137 | """ 138 | Derive a key from a from signMessage from a node. 139 | param `sig` is a RFC6979 deterministic signature. 140 | """ 141 | hashing_key = SigningKey.from_string(sha256(sig).digest(), curve=SECP256k1, hashfunc=sha256) 142 | linking_key = SigningKey.from_string( 143 | hmac.digest(hashing_key.to_string(), domain.encode(), "sha256"), curve=SECP256k1, hashfunc=sha256 144 | ) 145 | assert linking_key.privkey and linking_key.verifying_key 146 | pubkey = linking_key.verifying_key.to_string("compressed") 147 | return linking_key.privkey, pubkey 148 | 149 | 150 | def _bech32_decode(bech32: str, *, allowed_hrp: Optional[Set[str]] = None) -> Tuple[str, List[int]]: 151 | hrp, data = bech32_decode(bech32) 152 | 153 | if not hrp or not data or (allowed_hrp and hrp not in allowed_hrp): 154 | raise ValueError(f"Invalid data or Human Readable Prefix (HRP): {hrp}.") 155 | 156 | return hrp, data 157 | 158 | 159 | def _lnurl_clean(lnurl: str) -> str: 160 | lnurl = lnurl.strip() 161 | return lnurl.replace("lightning:", "") if lnurl.startswith("lightning:") else lnurl 162 | 163 | 164 | def url_decode(lnurl: str) -> str: 165 | """ 166 | Decode a LNURL and return a url string without performing any validation on it. 167 | Use `lnurl.decode()` for validation and to get `Url` object. 168 | """ 169 | _, data = _bech32_decode(_lnurl_clean(lnurl), allowed_hrp={"lnurl"}) 170 | 171 | try: 172 | bech32_data = convertbits(data, 5, 8, False) 173 | assert bech32_data 174 | url = bytes(bech32_data).decode("utf-8") 175 | return url 176 | except UnicodeDecodeError: 177 | raise InvalidLnurl 178 | 179 | 180 | def url_encode(url: str) -> str: 181 | """ 182 | Encode a URL without validating it first and return a bech32 LNURL string. 183 | Use `lnurl.encode()` for validation and to get a `Lnurl` object. 184 | """ 185 | try: 186 | bech32_data = convertbits(url.encode("utf-8"), 8, 5, True) 187 | assert bech32_data 188 | lnurl = bech32_encode("lnurl", bech32_data) 189 | except UnicodeEncodeError: 190 | raise InvalidUrl 191 | 192 | return lnurl.upper() 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LNURL implementation for Python 2 | =============================== 3 | 4 | [![github-tests-badge]][github-tests] 5 | [![github-mypy-badge]][github-mypy] 6 | [![codecov-badge]][codecov] 7 | [![pypi-badge]][pypi] 8 | [![pypi-versions-badge]][pypi] 9 | [![license-badge]](LICENSE) 10 | 11 | 12 | A collection of helpers for building [LNURL][lnurl] support into wallets and services. 13 | 14 | 15 | LUDS support 16 | ------------ 17 | 18 | Check out the LUDS repository: [luds](https://github.com/lnurl/luds/) 19 | 20 | - [x] LUD-01 - Base LNURL encoding and decoding 21 | - [x] LUD-02 - channelRequest base spec 22 | - [x] LUD-03 - withdrawRequest base spec 23 | - [x] LUD-04 - Auth base spec 24 | - [x] LUD-05 - BIP32-based seed generation for auth protocol 25 | - [x] LUD-06 - payRequest base spec 26 | - [x] LUD-07 - hostedChannelRequest base spec 27 | - [x] LUD-08 - Fast withdrawRequest 28 | - [x] LUD-09 - successAction field for payRequest 29 | - [x] LUD-10 - aes success action in payRequest 30 | - [x] LUD-11 - Disposable and storeable payRequests 31 | - [x] LUD-12 - Comments in payRequest 32 | - [x] LUD-13 - signMessage-based seed generation for auth protocol 33 | - [x] LUD-14 - balanceCheck: reusable withdrawRequests 34 | - [x] LUD-15 - balanceNotify: services hurrying up the withdraw process 35 | - [x] LUD-16 - Paying to static internet identifiers 36 | - [x] LUD-17 - Scheme prefixes and raw (non bech32-encoded) URLs 37 | - [x] LUD-18 - Payer identity in payRequest protocol 38 | - [x] LUD-19 - Pay link discoverable from withdraw link 39 | - [x] LUD-20 - Long payment description for pay protocol 40 | - [x] LUD-21 - verify LNURL-pay payments 41 | 42 | 43 | Configuration 44 | ------------- 45 | 46 | Developers can force strict RFC3986 validation for the URLs that the library encodes/decodes, using this env var: 47 | 48 | > LNURL_STRICT_RFC3986 = "0" by default (False) 49 | 50 | 51 | Basic usage 52 | ----------- 53 | 54 | ```python 55 | >>> import lnurl 56 | >>> lnurl.encode('https://service.io/?q=3fc3645b439ce8e7') 57 | Lnurl('LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWXQ96S9', bech32=Bech32('LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWXQ96S9', hrp='lnurl', data=[13, 1, 26, 7, 8, 28, 3, 19, 7, 8, 23, 18, 30, 28, 27, 5, 14, 9, 27, 6, 18, 24, 27, 5, 5, 25, 20, 22, 30, 11, 25, 31, 14, 4, 30, 19, 6, 25, 19, 3, 6, 12, 27, 3, 8, 13, 11, 2, 6, 16, 25, 19, 18, 24, 27, 5, 7, 1, 18, 19, 14]), url=WebUrl('https://service.io/?q=3fc3645b439ce8e7', scheme='https', host='service.io', tld='io', host_type='domain', path='/', query='q=3fc3645b439ce8e7')) 58 | >>> lnurl.decode('LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWXQ96S9') 59 | WebUrl('https://service.io/?q=3fc3645b439ce8e7', scheme='https', host='service.io', tld='io', host_type='domain', path='/', query='q=3fc3645b439ce8e7') 60 | ``` 61 | 62 | The `Lnurl` object wraps a bech32 LNURL to provide some extra utilities. 63 | 64 | ```python 65 | from lnurl import Lnurl 66 | 67 | lnurl = Lnurl("LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWXQ96S9") 68 | lnurl.bech32 # "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWXQ96S9" 69 | lnurl.bech32.hrp # "lnurl" 70 | lnurl.url # "https://service.io/?q=3fc3645b439ce8e7" 71 | lnurl.url.host # "service.io" 72 | lnurl.url.base # "https://service.io/" 73 | lnurl.url.query # "q=3fc3645b439ce8e7" 74 | lnurl.url.query_params # {"q": "3fc3645b439ce8e7"} 75 | ``` 76 | 77 | Parsing LNURL responses 78 | ----------------------- 79 | 80 | You can use a `LnurlResponse` to wrap responses you get from a LNURL. 81 | The different types of responses defined in the [LNURL spec][lnurl-spec] have a different model 82 | with different properties (see `models.py`): 83 | 84 | ```python 85 | import httpx 86 | 87 | from lnurl import Lnurl, LnurlResponse 88 | 89 | lnurl = Lnurl('LNURL1DP68GURN8GHJ7MRWW4EXCTNZD9NHXATW9EU8J730D3H82UNV94MKJARGV3EXZAELWDJHXUMFDAHR6WFHXQERSVPCA649RV') 90 | try: 91 | async with httpx.AsyncClient() as client: 92 | r = await client.get(lnurl.url) 93 | res = LnurlResponse.from_dict(r.json()) # LnurlPayResponse 94 | res.ok # bool 95 | res.maxSendable # int 96 | res.max_sats # int 97 | res.callback.base # str 98 | res.callback.query_params # dict 99 | res.metadata # str 100 | res.metadata.list() # list 101 | res.metadata.text # str 102 | res.metadata.images # list 103 | r = requests.get(lnurl.url) 104 | ``` 105 | 106 | If you have already `httpx` installed, you can also use the `.handle()` function directly. 107 | It will return the appropriate response for a LNURL. 108 | 109 | ```python 110 | >>> import lnurl 111 | >>> lnurl.handle('lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTNZD9NHXATW9EU8J730D3H82UNV94CXZ7FLWDJHXUMFDAHR6V33XCUNSVE38QV6UF') 112 | LnurlPayResponse(tag='payRequest', callback=WebUrl('https://lnurl.bigsun.xyz/lnurl-pay/callback/2169831', scheme='https', host='lnurl.bigsun.xyz', tld='xyz', host_type='domain', path='/lnurl-pay/callback/2169831'), minSendable=10000, maxSendable=10000, metadata=LnurlPayMetadata('[["text/plain","NgHaEyaZNDnW iI DsFYdkI"],["image/png;base64","iVBOR...uQmCC"]]')) 113 | ``` 114 | 115 | You can execute and LNURL with either payRequest, withdrawRequest or login tag using the `execute` function. 116 | ```python 117 | >>> import lnurl 118 | >>> lnurl.execute('lightning:LNURL1DP68GURN8GHJ7MRWW4EXCTNZD9NHXATW9EU8J730D3H82UNV94CXZ7FLWDJHXUMFDAHR6V33XCUNSVE38QV6UF', 100000) 119 | ``` 120 | 121 | Building your own LNURL responses 122 | --------------------------------- 123 | 124 | For LNURL services, the `lnurl` package can be used to build **valid** responses. 125 | 126 | ```python 127 | from lnurl import CallbackUrl, LnurlWithdrawResponse, MilliSatoshi 128 | from pydantic import parse_obj_as, ValidationError 129 | try: 130 | res = LnurlWithdrawResponse( 131 | callback=parse_obj_as(CallbackUrl, "https://lnurl.bigsun.xyz/lnurl-withdraw/callback/9702808"), 132 | k1="38d304051c1b76dcd8c5ee17ee15ff0ebc02090c0afbc6c98100adfa3f920874", 133 | minWithdrawable=MilliSatoshi(1000), 134 | maxWithdrawable=MilliSatoshi(1000000), 135 | defaultDescription="sample withdraw", 136 | ) 137 | res.json() # str 138 | res.dict() # dict 139 | except ValidationError as e: 140 | print(e.json()) 141 | ``` 142 | 143 | All responses are `pydantic` models, so the information you provide will be validated and you have 144 | access to `.json()` and `.dict()` methods to export the data. 145 | 146 | **Data is exported using :camel: camelCase keys by default, as per spec.** 147 | You can also use camelCases when you parse the data, and it will be converted to snake_case to make your 148 | Python code nicer. 149 | 150 | Will throw and ValidationError if the data is not valid, so you can catch it and return an error response. 151 | 152 | 153 | [github-tests]: https://github.com/lnbits/lnurl/actions?query=workflow%3Atests 154 | [github-tests-badge]: https://github.com/lnbits/lnurl/workflows/tests/badge.svg 155 | [github-mypy]: https://github.com/lnbits/lnurl/actions?query=workflow%3Amypy 156 | [github-mypy-badge]: https://github.com/lnbits/lnurl/workflows/mypy/badge.svg 157 | [codecov]: https://codecov.io/gh/lnbits/lnurl 158 | [codecov-badge]: https://codecov.io/gh/lnbits/lnurl/branch/master/graph/badge.svg 159 | [pypi]: https://pypi.org/project/lnurl/ 160 | [pypi-badge]: https://badge.fury.io/py/lnurl.svg 161 | [pypi-versions-badge]: https://img.shields.io/pypi/pyversions/lnurl.svg 162 | [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg 163 | 164 | 165 | CLI 166 | --------- 167 | ```console 168 | $ poetry run lnurl 169 | Usage: lnurl [OPTIONS] COMMAND [ARGS]... 170 | 171 | Python CLI for LNURL decode and encode lnurls 172 | 173 | Options: 174 | --help Show this message and exit. 175 | 176 | Commands: 177 | decode decode a LNURL 178 | encode encode a URL 179 | handle handle a LNURL 180 | execute execute a LNURL 181 | ``` 182 | -------------------------------------------------------------------------------- /lnurl/core.py: -------------------------------------------------------------------------------- 1 | from json import JSONDecodeError 2 | from typing import Any, Optional 3 | 4 | import httpx 5 | from bolt11 import Bolt11Exception, MilliSatoshi 6 | from bolt11 import decode as bolt11_decode 7 | from pydantic import ValidationError, parse_obj_as 8 | 9 | from .exceptions import InvalidLnurl, InvalidUrl, LnurlResponseException 10 | from .helpers import ( 11 | lnurlauth_derive_linking_key, 12 | lnurlauth_derive_linking_key_sign_message, 13 | lnurlauth_signature, 14 | url_encode, 15 | ) 16 | from .models import ( 17 | LnurlAuthResponse, 18 | LnurlErrorResponse, 19 | LnurlPayActionResponse, 20 | LnurlPayResponse, 21 | LnurlResponse, 22 | LnurlResponseModel, 23 | LnurlSuccessResponse, 24 | LnurlWithdrawResponse, 25 | ) 26 | from .types import CallbackUrl, LnAddress, Lnurl 27 | 28 | USER_AGENT = "lnbits/lnurl" 29 | TIMEOUT = 5 30 | 31 | 32 | def decode(lnurl: str) -> Lnurl: 33 | try: 34 | return Lnurl(lnurl) 35 | except (ValidationError, ValueError): 36 | raise InvalidLnurl 37 | 38 | 39 | def encode(url: str) -> Lnurl: 40 | try: 41 | return Lnurl(url_encode(url)) 42 | except (ValidationError, ValueError): 43 | raise InvalidUrl 44 | 45 | 46 | async def get( 47 | url: str, 48 | *, 49 | response_class: Optional[Any] = None, 50 | user_agent: Optional[str] = None, 51 | timeout: Optional[int] = None, 52 | ) -> LnurlResponseModel: 53 | headers = {"User-Agent": user_agent or USER_AGENT} 54 | async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client: 55 | try: 56 | res = await client.get(url, timeout=timeout or TIMEOUT) 57 | res.raise_for_status() 58 | except Exception as exc: 59 | raise LnurlResponseException(str(exc)) from exc 60 | 61 | try: 62 | _json = res.json() 63 | except JSONDecodeError as exc: 64 | raise LnurlResponseException(f"Invalid JSON response from {url}") from exc 65 | 66 | if response_class: 67 | if not issubclass(response_class, LnurlResponseModel): 68 | raise LnurlResponseException("response_class must be a subclass of LnurlResponseModel") 69 | return response_class(**_json) 70 | 71 | return LnurlResponse.from_dict(_json) 72 | 73 | 74 | async def handle( 75 | lnurl: str, 76 | response_class: Optional[LnurlResponseModel] = None, 77 | user_agent: Optional[str] = None, 78 | timeout: Optional[int] = None, 79 | ) -> LnurlResponseModel: 80 | try: 81 | if "@" in lnurl: 82 | lnaddress = LnAddress(lnurl) 83 | return await get(lnaddress.url, response_class=response_class, user_agent=user_agent, timeout=timeout) 84 | lnurl = Lnurl(lnurl) 85 | except (ValidationError, ValueError): 86 | raise InvalidLnurl 87 | 88 | if lnurl.is_login: 89 | callback_url = parse_obj_as(CallbackUrl, lnurl.url) 90 | return LnurlAuthResponse(callback=callback_url, k1=lnurl.url.query_params["k1"]) 91 | 92 | return await get(lnurl.url, response_class=response_class, user_agent=user_agent, timeout=timeout) 93 | 94 | 95 | async def execute( 96 | bech32_or_address: str, 97 | value: str, 98 | user_agent: Optional[str] = None, 99 | timeout: Optional[int] = None, 100 | ) -> LnurlResponseModel: 101 | try: 102 | res = await handle(bech32_or_address, user_agent=user_agent, timeout=timeout) 103 | except Exception as exc: 104 | raise LnurlResponseException(str(exc)) 105 | 106 | if isinstance(res, LnurlPayResponse) and res.tag == "payRequest": 107 | return await execute_pay_request(res, int(value), user_agent=user_agent, timeout=timeout) 108 | elif isinstance(res, LnurlAuthResponse) and res.tag == "login": 109 | return await execute_login(res, value, user_agent=user_agent, timeout=timeout) 110 | elif isinstance(res, LnurlWithdrawResponse) and res.tag == "withdrawRequest": 111 | return await execute_withdraw(res, value, user_agent=user_agent, timeout=timeout) 112 | 113 | raise LnurlResponseException("tag not implemented") 114 | 115 | 116 | async def execute_pay_request( 117 | res: LnurlPayResponse, 118 | msat: int, 119 | comment: Optional[str] = None, 120 | user_agent: Optional[str] = None, 121 | timeout: Optional[int] = None, 122 | ) -> LnurlPayActionResponse: 123 | if not res.minSendable <= MilliSatoshi(msat) <= res.maxSendable: 124 | raise LnurlResponseException(f"Amount {msat} not in range {res.minSendable} - {res.maxSendable}") 125 | 126 | params: dict[str, str | int] = {"amount": msat} 127 | 128 | if res.commentAllowed and comment: 129 | if len(comment) > res.commentAllowed: 130 | raise LnurlResponseException(f"Comment length {len(comment)} exceeds allowed length {res.commentAllowed}") 131 | params["comment"] = comment 132 | 133 | try: 134 | headers = {"User-Agent": user_agent or USER_AGENT} 135 | async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client: 136 | res2 = await client.get( 137 | url=res.callback, 138 | params=params, 139 | timeout=timeout or TIMEOUT, 140 | ) 141 | res2.raise_for_status() 142 | pay_res = LnurlResponse.from_dict(res2.json()) 143 | if isinstance(pay_res, LnurlErrorResponse): 144 | raise LnurlResponseException(pay_res.reason) 145 | if not isinstance(pay_res, LnurlPayActionResponse): 146 | raise LnurlResponseException(f"Expected LnurlPayActionResponse, got {type(pay_res)}") 147 | invoice = bolt11_decode(pay_res.pr) 148 | if invoice.amount_msat != int(msat): 149 | raise LnurlResponseException( 150 | f"{res.callback.host} returned an invalid invoice." 151 | f"Excepted `{msat}` msat, got `{invoice.amount_msat}`." 152 | ) 153 | return pay_res 154 | except Exception as exc: 155 | raise LnurlResponseException(str(exc)) 156 | 157 | 158 | async def execute_login( 159 | res: LnurlAuthResponse, 160 | seed: str | None = None, 161 | signed_message: str | None = None, 162 | user_agent: Optional[str] = None, 163 | timeout: Optional[int] = None, 164 | ) -> LnurlResponseModel: 165 | if not res.callback: 166 | raise LnurlResponseException("LNURLauth callback does not exist") 167 | host = res.callback.host 168 | if not host: 169 | raise LnurlResponseException("Invalid host in LNURLauth callback") 170 | if seed: 171 | linking_key, _ = lnurlauth_derive_linking_key(seed=seed, domain=host) 172 | elif signed_message: 173 | linking_key, _ = lnurlauth_derive_linking_key_sign_message(domain=host, sig=signed_message.encode()) 174 | else: 175 | raise LnurlResponseException("Seed or signed_message is required for LNURLauth") 176 | try: 177 | key, sig = lnurlauth_signature(res.k1, linking_key=linking_key) 178 | headers = {"User-Agent": user_agent or USER_AGENT} 179 | async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client: 180 | res2 = await client.get( 181 | url=res.callback, 182 | params={ 183 | "key": key, 184 | "sig": sig, 185 | }, 186 | timeout=timeout or TIMEOUT, 187 | ) 188 | res2.raise_for_status() 189 | return LnurlResponse.from_dict(res2.json()) 190 | except Exception as e: 191 | raise LnurlResponseException(str(e)) 192 | 193 | 194 | async def execute_withdraw( 195 | res: LnurlWithdrawResponse, 196 | pr: str, 197 | user_agent: Optional[str] = None, 198 | timeout: Optional[int] = None, 199 | ) -> LnurlSuccessResponse: 200 | try: 201 | invoice = bolt11_decode(pr) 202 | except Bolt11Exception as exc: 203 | raise LnurlResponseException(str(exc)) 204 | # if invoice does not have amount use the min withdrawable amount 205 | amount = invoice.amount_msat or res.minWithdrawable 206 | if not res.minWithdrawable <= MilliSatoshi(amount) <= res.maxWithdrawable: 207 | raise LnurlResponseException(f"Amount {amount} not in range {res.minWithdrawable} - {res.maxWithdrawable}") 208 | try: 209 | headers = {"User-Agent": user_agent or USER_AGENT} 210 | async with httpx.AsyncClient(headers=headers, follow_redirects=True) as client: 211 | res2 = await client.get( 212 | url=res.callback, 213 | params={ 214 | "k1": res.k1, 215 | "pr": pr, 216 | }, 217 | timeout=timeout or TIMEOUT, 218 | ) 219 | res2.raise_for_status() 220 | withdraw_res = LnurlResponse.from_dict(res2.json()) 221 | if isinstance(withdraw_res, LnurlErrorResponse): 222 | raise LnurlResponseException(withdraw_res.reason) 223 | if not isinstance(withdraw_res, LnurlSuccessResponse): 224 | raise LnurlResponseException(f"Expected LnurlSuccessResponse, got {type(withdraw_res)}") 225 | return withdraw_res 226 | except Exception as exc: 227 | raise LnurlResponseException(str(exc)) 228 | -------------------------------------------------------------------------------- /lnurl/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | from abc import ABC 5 | from typing import Optional, Union 6 | 7 | from bolt11 import MilliSatoshi 8 | from pydantic import BaseModel, Field, ValidationError, validator 9 | 10 | from .exceptions import LnurlResponseException 11 | from .types import ( 12 | CallbackUrl, 13 | CiphertextBase64, 14 | InitializationVectorBase64, 15 | LightningInvoice, 16 | LightningNodeUri, 17 | Lnurl, 18 | LnurlPayMetadata, 19 | LnurlPaySuccessActionTag, 20 | LnurlResponseTag, 21 | LnurlStatus, 22 | Max144Str, 23 | Url, 24 | ) 25 | 26 | 27 | class LnurlPayRouteHop(BaseModel): 28 | nodeId: str 29 | channelUpdate: str 30 | 31 | 32 | class LnurlPaySuccessAction(BaseModel, ABC): 33 | tag: LnurlPaySuccessActionTag 34 | 35 | 36 | class MessageAction(LnurlPaySuccessAction): 37 | tag: LnurlPaySuccessActionTag = LnurlPaySuccessActionTag.message 38 | message: Max144Str 39 | 40 | 41 | class UrlAction(LnurlPaySuccessAction): 42 | tag: LnurlPaySuccessActionTag = LnurlPaySuccessActionTag.url 43 | url: Url 44 | description: Max144Str 45 | 46 | 47 | # LUD-10: Add support for AES encrypted messages in payRequest. 48 | class AesAction(LnurlPaySuccessAction): 49 | tag: LnurlPaySuccessActionTag = LnurlPaySuccessActionTag.aes 50 | description: Max144Str 51 | ciphertext: CiphertextBase64 52 | iv: InitializationVectorBase64 53 | 54 | 55 | class LnurlResponseModel(BaseModel): 56 | 57 | class Config: 58 | use_enum_values = True 59 | extra = "forbid" 60 | 61 | def dict(self, **kwargs): 62 | kwargs["exclude_none"] = True 63 | return super().dict(**kwargs) 64 | 65 | def json(self, **kwargs): 66 | kwargs["exclude_none"] = True 67 | return super().json(**kwargs) 68 | 69 | @property 70 | def ok(self) -> bool: 71 | return True 72 | 73 | 74 | class LnurlErrorResponse(LnurlResponseModel): 75 | status: LnurlStatus = LnurlStatus.error 76 | reason: str 77 | 78 | @property 79 | def error_msg(self) -> str: 80 | return self.reason 81 | 82 | @property 83 | def ok(self) -> bool: 84 | return False 85 | 86 | 87 | class LnurlSuccessResponse(LnurlResponseModel): 88 | status: LnurlStatus = LnurlStatus.ok 89 | 90 | 91 | # LUD-21: verify base spec. 92 | class LnurlPayVerifyResponse(LnurlSuccessResponse): 93 | pr: LightningInvoice = Field(description="Payment request") 94 | settled: bool = Field(description="Settled status of the payment") 95 | preimage: Optional[str] = Field(default=None, description="Preimage of the payment") 96 | 97 | 98 | # LUD-04: auth base spec. 99 | class LnurlAuthResponse(LnurlResponseModel): 100 | tag: LnurlResponseTag = LnurlResponseTag.login 101 | callback: CallbackUrl 102 | k1: str 103 | 104 | 105 | # LUD-2: channelRequest base spec. 106 | class LnurlChannelResponse(LnurlResponseModel): 107 | tag: LnurlResponseTag = LnurlResponseTag.channelRequest 108 | uri: LightningNodeUri 109 | callback: CallbackUrl 110 | k1: str 111 | 112 | 113 | # LUD-07: hostedChannelRequest base spec. 114 | class LnurlHostedChannelResponse(LnurlResponseModel): 115 | tag: LnurlResponseTag = LnurlResponseTag.hostedChannelRequest 116 | uri: LightningNodeUri 117 | k1: str 118 | alias: Optional[str] = None 119 | 120 | 121 | # LUD-18: Payer identity in payRequest protocol. 122 | class LnurlPayResponsePayerDataOption(BaseModel): 123 | mandatory: bool 124 | 125 | 126 | class LnurlPayResponsePayerDataOptionAuth(LnurlPayResponsePayerDataOption): 127 | k1: str 128 | 129 | 130 | class LnurlPayResponsePayerDataExtra(BaseModel): 131 | name: str 132 | field: LnurlPayResponsePayerDataOption 133 | 134 | 135 | class LnurlPayResponsePayerData(BaseModel): 136 | name: Optional[LnurlPayResponsePayerDataOption] = None 137 | pubkey: Optional[LnurlPayResponsePayerDataOption] = None 138 | identifier: Optional[LnurlPayResponsePayerDataOption] = None 139 | email: Optional[LnurlPayResponsePayerDataOption] = None 140 | auth: Optional[LnurlPayResponsePayerDataOptionAuth] = None 141 | extras: Optional[list[LnurlPayResponsePayerDataExtra]] = None 142 | 143 | 144 | class LnurlPayerDataAuth(BaseModel): 145 | key: str 146 | k1: str 147 | sig: str 148 | 149 | 150 | class LnurlPayerData(BaseModel): 151 | name: Optional[str] = None 152 | pubkey: Optional[str] = None 153 | identifier: Optional[str] = None 154 | email: Optional[str] = None 155 | auth: Optional[LnurlPayerDataAuth] = None 156 | extras: Optional[dict] = None 157 | 158 | 159 | class LnurlPayResponse(LnurlResponseModel): 160 | tag: LnurlResponseTag = LnurlResponseTag.payRequest 161 | callback: CallbackUrl 162 | minSendable: MilliSatoshi = Field(gt=0) 163 | maxSendable: MilliSatoshi = Field(gt=0) 164 | metadata: LnurlPayMetadata 165 | # LUD-18: Payer identity in payRequest protocol. 166 | payerData: Optional[LnurlPayResponsePayerData] = None 167 | # Adds the optional comment_allowed field to the LnurlPayResponse 168 | # ref LUD-12: Comments in payRequest. 169 | commentAllowed: Optional[int] = None 170 | 171 | # NIP-57 Lightning Zaps 172 | allowsNostr: Optional[bool] = None 173 | nostrPubkey: Optional[str] = None 174 | 175 | @validator("maxSendable") 176 | def max_less_than_min(cls, value, values): # noqa 177 | if "minSendable" in values and value < values["minSendable"]: 178 | raise ValueError("`maxSendable` cannot be less than `minSendable`.") 179 | return value 180 | 181 | @property 182 | def min_sats(self) -> int: 183 | return int(math.ceil(self.minSendable / 1000)) 184 | 185 | @property 186 | def max_sats(self) -> int: 187 | return int(math.floor(self.maxSendable / 1000)) 188 | 189 | def is_valid_amount(self, amount_msat: int) -> bool: 190 | return self.minSendable <= amount_msat <= self.maxSendable 191 | 192 | 193 | class LnurlPayActionResponse(LnurlResponseModel): 194 | pr: LightningInvoice 195 | # LUD-9: successAction field for payRequest. 196 | successAction: Optional[Union[AesAction, MessageAction, UrlAction]] = None 197 | routes: list[list[LnurlPayRouteHop]] = [] 198 | # LUD-11: Disposable and storeable payRequests. 199 | # If disposable is null, it should be interpreted as true. 200 | # so if SERVICE intends its LNURL links to be stored it must return disposable=False. 201 | disposable: Optional[bool] = Field(default=None, description="LUD-11: Disposable and storeable payRequests.") 202 | # LUD-21: verify base spec. 203 | verify: Optional[CallbackUrl] = Field(default=None, description="LUD-21: verify base spec.") 204 | 205 | 206 | class LnurlWithdrawResponse(LnurlResponseModel): 207 | tag: LnurlResponseTag = LnurlResponseTag.withdrawRequest 208 | callback: CallbackUrl 209 | k1: str 210 | minWithdrawable: MilliSatoshi = Field(ge=0) 211 | maxWithdrawable: MilliSatoshi = Field(ge=0) 212 | defaultDescription: str = "" 213 | # LUD-14: balanceCheck: reusable withdrawRequests 214 | balanceCheck: Optional[CallbackUrl] = None 215 | currentBalance: Optional[MilliSatoshi] = None 216 | # LUD-19: Pay link discoverable from withdraw link. 217 | payLink: Optional[str] = None 218 | 219 | @validator("payLink", pre=True) 220 | def paylink_must_be_lud17(cls, value: Optional[str] = None) -> str | None: 221 | if not value: 222 | return None 223 | lnurl = Lnurl(value) 224 | if lnurl.is_lud17 and lnurl.lud17_prefix == "lnurlp": 225 | return value 226 | raise ValueError("`payLink` must be a valid LUD17 URL (lnurlp://).") 227 | 228 | @validator("maxWithdrawable") 229 | def max_less_than_min(cls, value, values): 230 | if "minWithdrawable" in values and value < values["minWithdrawable"]: 231 | raise ValueError("`maxWithdrawable` cannot be less than `minWithdrawable`.") 232 | return value 233 | 234 | # LUD-08: Fast withdrawRequest. 235 | @property 236 | def fast_withdraw_query(self) -> str: 237 | return "&".join([f"{k}={v}" for k, v in self.dict().items()]) 238 | 239 | @property 240 | def min_sats(self) -> int: 241 | return int(math.ceil(self.minWithdrawable / 1000)) 242 | 243 | @property 244 | def max_sats(self) -> int: 245 | return int(math.floor(self.maxWithdrawable / 1000)) 246 | 247 | def is_valid_amount(self, amount: int) -> bool: 248 | return self.minWithdrawable <= amount <= self.maxWithdrawable 249 | 250 | 251 | def is_pay_action_response(data: dict) -> bool: 252 | return "pr" in data and "routes" in data 253 | 254 | 255 | class LnurlResponse: 256 | 257 | @staticmethod 258 | def from_dict(data: dict) -> LnurlResponseModel: 259 | tag = data.get("tag") 260 | 261 | # drop status field from all responses with a tag 262 | # some services return `status` in responses with a tag 263 | if tag or is_pay_action_response(data): 264 | data.pop("status", None) 265 | 266 | try: 267 | if tag == "channelRequest": 268 | return LnurlChannelResponse(**data) 269 | elif tag == "hostedChannelRequest": 270 | return LnurlHostedChannelResponse(**data) 271 | elif tag == "payRequest": 272 | return LnurlPayResponse(**data) 273 | elif tag == "withdrawRequest": 274 | return LnurlWithdrawResponse(**data) 275 | elif is_pay_action_response(data): 276 | return LnurlPayActionResponse(**data) 277 | 278 | except ValidationError as exc: 279 | raise LnurlResponseException(str(exc)) from exc 280 | 281 | status = data.get("status") 282 | if status is None or status == "": 283 | raise LnurlResponseException("Expected Success or Error response. But no `status` given.") 284 | 285 | # some services return `status` in lowercase, but spec says upper 286 | status = status.upper() 287 | 288 | if status == "OK": 289 | return LnurlSuccessResponse(status=LnurlStatus.ok) 290 | 291 | if status == "ERROR": 292 | return LnurlErrorResponse(status=LnurlStatus.error, reason=data.get("reason", "Unknown error")) 293 | 294 | # if we reach here, it's an unknown response 295 | raise LnurlResponseException(f"Unknown response: {data}") 296 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from pydantic import ValidationError, parse_obj_as 5 | 6 | from lnurl import CallbackUrl, Lnurl, LnurlPayMetadata, MilliSatoshi, encode 7 | from lnurl.models import ( 8 | LnurlChannelResponse, 9 | LnurlErrorResponse, 10 | LnurlHostedChannelResponse, 11 | LnurlPayResponse, 12 | LnurlSuccessResponse, 13 | LnurlWithdrawResponse, 14 | ) 15 | 16 | 17 | class TestLnurlErrorResponse: 18 | def test_response(self): 19 | res = LnurlErrorResponse(reason="blah blah blah") 20 | assert res.ok is False 21 | assert res.error_msg == "blah blah blah" 22 | assert res.json() == '{"status": "ERROR", "reason": "blah blah blah"}' 23 | assert res.dict() == {"status": "ERROR", "reason": "blah blah blah"} 24 | 25 | 26 | class TestLnurlSuccessResponse: 27 | def test_success_response(self): 28 | res = LnurlSuccessResponse() 29 | assert res.ok 30 | assert res.json() == '{"status": "OK"}' 31 | assert res.dict() == {"status": "OK"} 32 | 33 | 34 | class TestLnurlChannelResponse: 35 | @pytest.mark.parametrize( 36 | "d", [{"uri": "node_key@ip_address:port_number", "callback": "https://service.io/channel", "k1": "c3RyaW5n"}] 37 | ) 38 | def test_channel_response(self, d): 39 | res = LnurlChannelResponse(**d) 40 | assert res.ok 41 | assert res.dict() == {**{"tag": "channelRequest"}, **d} 42 | 43 | @pytest.mark.parametrize( 44 | "d", 45 | [ 46 | {"uri": "invalid", "callback": "https://service.io/channel", "k1": "c3RyaW5n"}, 47 | {"uri": "node_key@ip_address:port_number", "callback": "invalid", "k1": "c3RyaW5n"}, 48 | {"uri": "node_key@ip_address:port_number", "callback": "https://service.io/channel", "k1": None}, 49 | ], 50 | ) 51 | def test_invalid_data(self, d): 52 | with pytest.raises(ValidationError): 53 | LnurlChannelResponse(**d) 54 | 55 | 56 | class TestLnurlHostedChannelResponse: 57 | @pytest.mark.parametrize("d", [{"uri": "node_key@ip_address:port_number", "k1": "c3RyaW5n"}]) 58 | def test_channel_response(self, d): 59 | res = LnurlHostedChannelResponse(**d) 60 | assert res.ok 61 | assert res.dict() == {**{"tag": "hostedChannelRequest"}, **d} 62 | 63 | @pytest.mark.parametrize( 64 | "d", [{"uri": "invalid", "k1": "c3RyaW5n"}, {"uri": "node_key@ip_address:port_number", "k1": None}] 65 | ) 66 | def test_invalid_data(self, d): 67 | with pytest.raises(ValidationError): 68 | LnurlHostedChannelResponse(**d) 69 | 70 | 71 | metadata = '[["text/plain","lorem ipsum blah blah"]]' 72 | 73 | 74 | class TestLnurlPayResponse: 75 | @pytest.mark.parametrize( 76 | "callback, min_sendable, max_sendable, metadata", 77 | [ 78 | ( 79 | "https://service.io/pay", 80 | 1000, 81 | 2000, 82 | metadata, 83 | ), 84 | ], 85 | ) 86 | def test_success_response(self, callback: str, min_sendable: int, max_sendable: int, metadata: str): 87 | callback_url = parse_obj_as(CallbackUrl, callback) 88 | data = parse_obj_as(LnurlPayMetadata, metadata) 89 | res = LnurlPayResponse( 90 | callback=callback_url, 91 | minSendable=MilliSatoshi(min_sendable), 92 | maxSendable=MilliSatoshi(max_sendable), 93 | metadata=data, 94 | ) 95 | assert res.ok 96 | assert ( 97 | res.json() == res.json() == '{"tag": "payRequest", "callback": "https://service.io/pay", ' 98 | f'"minSendable": 1000, "maxSendable": 2000, "metadata": {json.dumps(metadata)}}}' 99 | ) 100 | assert ( 101 | res.dict() 102 | == res.dict() 103 | == { 104 | "tag": "payRequest", 105 | "callback": "https://service.io/pay", 106 | "minSendable": 1000, 107 | "maxSendable": 2000, 108 | "metadata": metadata, 109 | } 110 | ) 111 | 112 | @pytest.mark.parametrize( 113 | "d", 114 | [ 115 | {"callback": "invalid", "minSendable": 1000, "maxSendable": 2000, "metadata": metadata}, 116 | {"callback": "https://service.io/pay"}, # missing fields 117 | {"callback": "https://service.io/pay", "minSendable": 0, "maxSendable": 0, "metadata": metadata}, # 0 118 | {"callback": "https://service.io/pay", "minSendable": 100, "maxSendable": 10, "metadata": metadata}, # max 119 | {"callback": "https://service.io/pay", "minSendable": -90, "maxSendable": -10, "metadata": metadata}, 120 | ], 121 | ) 122 | def test_invalid_data(self, d): 123 | with pytest.raises(ValidationError): 124 | LnurlPayResponse(**d) 125 | 126 | 127 | class TestLnurlPayResponseComment: 128 | @pytest.mark.parametrize( 129 | "callback, min_sendable, max_sendable, metadata, comment_allowed", 130 | [ 131 | ( 132 | "https://service.io/pay", 133 | 1000, 134 | 2000, 135 | metadata, 136 | 555, # comment allowed 137 | ), 138 | ], 139 | ) 140 | def test_success_response( 141 | self, callback: str, min_sendable: int, max_sendable: int, metadata: str, comment_allowed: int 142 | ): 143 | res = LnurlPayResponse( 144 | callback=parse_obj_as(CallbackUrl, callback), 145 | minSendable=MilliSatoshi(min_sendable), 146 | maxSendable=MilliSatoshi(max_sendable), 147 | metadata=parse_obj_as(LnurlPayMetadata, metadata), 148 | commentAllowed=comment_allowed, 149 | ) 150 | assert res.ok 151 | assert ( 152 | res.json() == res.json() == '{"tag": "payRequest", "callback": "https://service.io/pay", ' 153 | f'"minSendable": 1000, "maxSendable": 2000, "metadata": {json.dumps(metadata)}, ' 154 | '"commentAllowed": 555}' 155 | ) 156 | assert ( 157 | res.dict() 158 | == res.dict() 159 | == { 160 | "tag": "payRequest", 161 | "callback": "https://service.io/pay", 162 | "minSendable": 1000, 163 | "maxSendable": 2000, 164 | "metadata": metadata, 165 | "commentAllowed": 555, 166 | } 167 | ) 168 | 169 | @pytest.mark.parametrize( 170 | "d", 171 | [ 172 | {"callback": "invalid", "minSendable": 1000, "maxSendable": 2000, "metadata": metadata}, 173 | {"callback": "https://service.io/pay"}, # missing fields 174 | {"callback": "https://service.io/pay", "minSendable": 0, "maxSendable": 0, "metadata": metadata}, # 0 175 | {"callback": "https://service.io/pay", "minSendable": 100, "maxSendable": 10, "metadata": metadata}, # max 176 | {"callback": "https://service.io/pay", "minSendable": -90, "maxSendable": -10, "metadata": metadata}, 177 | { 178 | "callback": "https://service.io/pay", 179 | "minSendable": 100, 180 | "maxSendable": 1000, 181 | "metadata": metadata, 182 | "commentAllowed": "Yes", # str should be int 183 | }, 184 | ], 185 | ) 186 | def test_invalid_data(self, d): 187 | with pytest.raises(ValidationError): 188 | LnurlPayResponse(**d) 189 | 190 | 191 | class TestLnurlWithdrawResponse: 192 | @pytest.mark.parametrize( 193 | "callback, k1, min_withdrawable, max_withdrawable", 194 | [ 195 | ( 196 | "https://service.io/withdraw", 197 | "c3RyaW5n", 198 | 100, 199 | 200, 200 | ), 201 | ], 202 | ) 203 | def test_success_response(self, callback: str, k1: str, min_withdrawable: int, max_withdrawable: int): 204 | res = LnurlWithdrawResponse( 205 | callback=parse_obj_as(CallbackUrl, callback), 206 | k1=k1, 207 | minWithdrawable=MilliSatoshi(min_withdrawable), 208 | maxWithdrawable=MilliSatoshi(max_withdrawable), 209 | ) 210 | assert res.ok 211 | assert ( 212 | res.json() 213 | == res.json() 214 | == '{"tag": "withdrawRequest", "callback": "https://service.io/withdraw", "k1": "c3RyaW5n", ' 215 | '"minWithdrawable": 100, "maxWithdrawable": 200, "defaultDescription": ""}' 216 | ) 217 | assert ( 218 | res.dict() 219 | == res.dict() 220 | == { 221 | "tag": "withdrawRequest", 222 | "callback": "https://service.io/withdraw", 223 | "k1": "c3RyaW5n", 224 | "minWithdrawable": 100, 225 | "maxWithdrawable": 200, 226 | "defaultDescription": "", 227 | } 228 | ) 229 | 230 | @pytest.mark.parametrize( 231 | "d", 232 | [ 233 | {"callback": "invalid", "k1": "c3RyaW5n", "minWithdrawable": 1000, "maxWithdrawable": 2000}, 234 | {"callback": "https://service.io/withdraw", "k1": "c3RyaW5n"}, # missing fields 235 | { 236 | "callback": "https://service.io/withdraw", 237 | "k1": "c3RyaW5n", 238 | "minWithdrawable": 100, 239 | "maxWithdrawable": 10, 240 | }, 241 | {"callback": "https://service.io/withdraw", "k1": "c3RyaW5n", "minWithdrawable": -9, "maxWithdrawable": -1}, 242 | ], 243 | ) 244 | def test_invalid_data(self, d): 245 | with pytest.raises(ValidationError): 246 | LnurlWithdrawResponse(**d) 247 | 248 | @pytest.mark.parametrize( 249 | "payLink", 250 | [ 251 | "https://service.io/withdraw", 252 | "lnurlw://service.io/withdraw", # wrong LUD17 253 | str(encode("https://service.io/withdraw").bech32), # bech32 254 | ], 255 | ) 256 | def test_invalid_pay_link(self, payLink: str): 257 | with pytest.raises(ValidationError): 258 | _ = LnurlWithdrawResponse( 259 | callback=parse_obj_as(CallbackUrl, "https://service.io/withdraw/cb"), 260 | k1="c3RyaW5n", 261 | minWithdrawable=MilliSatoshi(100), 262 | maxWithdrawable=MilliSatoshi(200), 263 | payLink=payLink, 264 | ) 265 | 266 | def test_valid_pay_link(self): 267 | payLink = parse_obj_as(Lnurl, "lnurlp://service.io/pay") 268 | assert payLink.is_lud17 269 | assert payLink.lud17_prefix == "lnurlp" 270 | _ = LnurlWithdrawResponse( 271 | callback=parse_obj_as(CallbackUrl, "https://service.io/withdraw/cb"), 272 | k1="c3RyaW5n", 273 | minWithdrawable=MilliSatoshi(100), 274 | maxWithdrawable=MilliSatoshi(200), 275 | payLink=payLink.lud17, 276 | ) 277 | -------------------------------------------------------------------------------- /lnurl/types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import os 5 | import re 6 | from enum import Enum 7 | from hashlib import sha256 8 | from typing import Generator, Optional 9 | from urllib.parse import parse_qs 10 | 11 | from pydantic import ( 12 | AnyUrl, 13 | ConstrainedStr, 14 | Json, 15 | ValidationError, 16 | parse_obj_as, 17 | validator, 18 | ) 19 | from pydantic.validators import str_validator 20 | 21 | from .exceptions import InvalidLnurlPayMetadata, InvalidUrl, LnAddressError 22 | from .helpers import _bech32_decode, _lnurl_clean, url_decode, url_encode 23 | 24 | INSECURE_HOSTS = ["127.0.0.1", "0.0.0.0", "localhost"] 25 | 26 | 27 | class ReprMixin: 28 | def __repr__(self) -> str: 29 | attrs = [ # type: ignore 30 | outer_slot # type: ignore 31 | for outer_slot in [slot for slot in self.__slots__ if not slot.startswith("_")] # type: ignore 32 | if getattr(self, outer_slot) # type: ignore 33 | ] # type: ignore 34 | extra = ", " + ", ".join(f"{n}={getattr(self, n).__repr__()}" for n in attrs) if attrs else "" 35 | return f"{self.__class__.__name__}({super().__repr__()}{extra})" 36 | 37 | 38 | class Bech32(ReprMixin, str): 39 | """Bech32 string.""" 40 | 41 | __slots__ = ("hrp", "data") 42 | 43 | def __new__(cls, bech32: str, **_) -> "Bech32": 44 | return str.__new__(cls, bech32) 45 | 46 | def __init__(self, bech32: str, *, hrp: Optional[str] = None, data: Optional[list[int]] = None): 47 | str.__init__(bech32) 48 | self.hrp, self.data = (hrp, data) if hrp and data else self.__get_data__(bech32) 49 | 50 | @classmethod 51 | def __get_data__(cls, bech32: str) -> tuple[str, list[int]]: 52 | return _bech32_decode(bech32) 53 | 54 | @classmethod 55 | def __get_validators__(cls): 56 | yield str_validator 57 | yield cls.validate 58 | 59 | @classmethod 60 | def validate(cls, value: str) -> "Bech32": 61 | hrp, data = cls.__get_data__(value) 62 | return cls(value, hrp=hrp, data=data) 63 | 64 | 65 | def ctrl_characters_validator(value: str) -> str: 66 | """Checks for control characters (unicode blocks C0 and C1, plus DEL).""" 67 | if re.compile(r"[\u0000-\u001f\u007f-\u009f]").search(value): 68 | raise InvalidUrl("URL contains control characters.") 69 | return value 70 | 71 | 72 | def strict_rfc3986_validator(value: str) -> str: 73 | """Checks for RFC3986 compliance.""" 74 | if os.environ.get("LNURL_STRICT_RFC3986", "0") == "1": 75 | if re.compile(r"[^]a-zA-Z0-9._~:/?#[@!$&'()*+,;=-]").search(value): 76 | raise InvalidUrl("URL is not RFC3986 compliant.") 77 | return value 78 | 79 | 80 | def valid_lnurl_host(url: str) -> AnyUrl: 81 | """Validates the host part of a URL.""" 82 | _url = parse_obj_as(AnyUrl, url) 83 | if not _url.host: 84 | raise InvalidUrl("URL host is required.") 85 | if _url.scheme == "http": 86 | if _url.host not in INSECURE_HOSTS and not _url.host.endswith(".onion"): 87 | raise InvalidUrl("HTTP scheme is only allowed for localhost or onion addresses.") 88 | return _url 89 | 90 | 91 | class Url(AnyUrl): 92 | max_length = 2047 # https://stackoverflow.com/questions/417142/ 93 | 94 | # LUD-17: Protocol schemes and raw (non bech32-encoded) URLs. 95 | allowed_schemes = {"https", "http", "lnurlc", "lnurlw", "lnurlp", "keyauth"} 96 | 97 | @property 98 | def is_lud17(self) -> bool: 99 | uris = ["lnurlc", "lnurlw", "lnurlp", "keyauth"] 100 | return any(self.scheme == uri for uri in uris) 101 | 102 | @classmethod 103 | def __get_validators__(cls) -> Generator: 104 | yield ctrl_characters_validator 105 | yield strict_rfc3986_validator 106 | yield valid_lnurl_host 107 | yield cls.validate 108 | 109 | @property 110 | def query_params(self) -> dict: 111 | return {k: v[0] for k, v in parse_qs(self.query).items()} 112 | 113 | @property 114 | def insecure(self) -> bool: 115 | if not self.host: 116 | return True 117 | return self.scheme == "http" or self.host in INSECURE_HOSTS or self.host.endswith(".onion") 118 | 119 | 120 | class CallbackUrl(Url): 121 | """URL for callbacks. exclude lud17 schemes.""" 122 | 123 | allowed_schemes = {"https", "http"} 124 | 125 | 126 | class LightningInvoice(Bech32): 127 | """Bech32 Lightning invoice.""" 128 | 129 | 130 | class LightningNodeUri(ReprMixin, str): 131 | """Remote node address of form `node_key@ip_address:port_number`.""" 132 | 133 | __slots__ = ("key", "ip", "port") 134 | 135 | def __new__(cls, uri: str, **_) -> "LightningNodeUri": 136 | return str.__new__(cls, uri) 137 | 138 | def __init__(self, uri: str, *, key: Optional[str] = None, ip: Optional[str] = None, port: Optional[str] = None): 139 | str.__init__(uri) 140 | self.key = key 141 | self.ip = ip 142 | self.port = port 143 | 144 | @classmethod 145 | def __get_validators__(cls): 146 | yield str_validator 147 | yield cls.validate 148 | 149 | @classmethod 150 | def validate(cls, value: str) -> "LightningNodeUri": 151 | try: 152 | key, netloc = value.split("@") 153 | ip, port = netloc.split(":") 154 | except Exception: 155 | raise ValueError 156 | 157 | return cls(value, key=key, ip=ip, port=port) 158 | 159 | 160 | class Lnurl(ReprMixin, str): 161 | url: Url 162 | lud17_prefix: Optional[str] = None 163 | 164 | def __new__(cls, lightning: str) -> Lnurl: 165 | url = cls.clean(lightning) 166 | _url = url.replace(url.scheme, "http" if url.insecure else "https", 1) 167 | return str.__new__(cls, _url) 168 | 169 | def __init__(self, lightning: str): 170 | url = self.clean(lightning) 171 | if not url.is_lud17: 172 | self.url = url 173 | self.lud17_prefix = None 174 | return str.__init__(url) 175 | self.lud17_prefix = url.scheme 176 | _url = parse_obj_as(Url, url.replace(url.scheme, "http" if url.insecure else "https", 1)) 177 | self.url = _url 178 | return str.__init__(_url) 179 | 180 | @classmethod 181 | def __get_validators__(cls): 182 | yield str_validator 183 | yield cls.validate 184 | 185 | @classmethod 186 | def clean(cls, lightning: str) -> Url: 187 | lightning = _lnurl_clean(lightning) 188 | if lightning.lower().startswith("lnurl1"): 189 | url = parse_obj_as(Url, url_decode(lightning)) 190 | return url 191 | url = parse_obj_as(Url, lightning) 192 | return url 193 | 194 | @classmethod 195 | def validate(cls, lightning: str) -> Lnurl: 196 | _ = cls.clean(lightning) 197 | return cls(lightning) 198 | 199 | @property 200 | def bech32(self) -> Bech32: 201 | """Returns Bech32 representation of the Lnurl if it is a Bech32 encoded URL.""" 202 | return parse_obj_as(Bech32, url_encode(self.url)) 203 | 204 | # LUD-04: auth base spec. 205 | @property 206 | def is_login(self) -> bool: 207 | return self.url.query_params.get("tag") == "login" 208 | 209 | # LUD-08: Fast withdrawRequest. 210 | @property 211 | def is_fast_withdraw(self) -> bool: 212 | q = self.url.query_params 213 | return ( 214 | q.get("tag") == "withdrawRequest" 215 | and q.get("k1") is not None 216 | and q.get("minWithdrawable") is not None 217 | and q.get("maxWithdrawable") is not None 218 | and q.get("defaultDescription") is not None 219 | and q.get("callback") is not None 220 | ) 221 | 222 | # LUD-17: Protocol schemes and raw (non bech32-encoded) URLs. 223 | @property 224 | def is_lud17(self) -> bool: 225 | return self.lud17_prefix is not None 226 | 227 | @property 228 | def lud17(self) -> Optional[str]: 229 | if not self.lud17_prefix: 230 | return None 231 | url = self.url.replace(self.url.scheme, self.lud17_prefix, 1) 232 | return url 233 | 234 | 235 | class LnAddress(ReprMixin, str): 236 | """Lightning address of form `user+tag@host`""" 237 | 238 | slots = ("address", "url", "tag") 239 | 240 | def __new__(cls, address: str) -> LnAddress: 241 | return str.__new__(cls, address) 242 | 243 | def __init__(self, address: str): 244 | str.__init__(address) 245 | if not self.is_valid_lnaddress(address): 246 | raise LnAddressError("Invalid Lightning address.") 247 | self.url = self.__get_url__(address) 248 | if "+" in address: 249 | self.tag: Optional[str] = address.split("+", 1)[1].split("@", 1)[0] 250 | self.address = address.replace(f"+{self.tag}", "", 1) 251 | else: 252 | self.tag = None 253 | self.address = address 254 | 255 | # LUD-16: Paying to static internet identifiers. 256 | @validator("address") 257 | def is_valid_lnaddress(cls, address: str) -> bool: 258 | # A user can then type these on a WALLET. The is limited 259 | # to a-z-1-9-_.. Please note that this is way more strict than common 260 | # email addresses as it allows fewer symbols and only lowercase characters. 261 | lnaddress_regex = r"[a-z0-9\._%+-]+@[A-Za-z0-9\.-]+\.[A-Za-z]{2,63}" 262 | return re.fullmatch(lnaddress_regex, address) is not None 263 | 264 | @classmethod 265 | def __get_url__(cls, address: str) -> CallbackUrl: 266 | name, domain = address.split("@") 267 | url = ("http://" if domain.endswith(".onion") else "https://") + domain + "/.well-known/lnurlp/" + name 268 | return parse_obj_as(CallbackUrl, url) 269 | 270 | 271 | class LnurlPayMetadata(ReprMixin, str): 272 | # LUD-16: Paying to static internet identifiers. "text/identifier", "text/email", "text/tag" 273 | # LUD-20: Long payment description for pay protocol. "text/long-desc" 274 | valid_metadata_mime_types = { 275 | "text/plain", 276 | "image/png;base64", 277 | "image/jpeg;base64", 278 | "text/identifier", 279 | "text/email", 280 | "text/tag", 281 | "text/long-desc", 282 | } 283 | 284 | __slots__ = ("_list",) 285 | 286 | def __new__(cls, json_str: str, **_) -> LnurlPayMetadata: 287 | return str.__new__(cls, json_str) 288 | 289 | def __init__(self, json_str: str, *, json_obj: Optional[list] = None): 290 | str.__init__(json_str) 291 | self._list = json_obj if json_obj else self.__validate_metadata__(json_str) 292 | 293 | @classmethod 294 | def __validate_metadata__(cls, json_str: str) -> list[tuple[str, str]]: 295 | try: 296 | parse_obj_as(Json[list[tuple[str, str]]], json_str) 297 | data = [(str(item[0]), str(item[1])) for item in json.loads(json_str)] 298 | except ValidationError: 299 | raise InvalidLnurlPayMetadata 300 | 301 | clean_data = [x for x in data if x[0] in cls.valid_metadata_mime_types] 302 | mime_types = [x[0] for x in clean_data] 303 | counts = {x: mime_types.count(x) for x in mime_types} 304 | 305 | if ( 306 | not clean_data 307 | or ("text/plain" not in mime_types and "text/long-desc" not in mime_types) 308 | or counts["text/plain"] > 1 309 | ): 310 | raise InvalidLnurlPayMetadata 311 | 312 | return clean_data 313 | 314 | @classmethod 315 | def __get_validators__(cls): 316 | yield str_validator 317 | yield cls.validate 318 | 319 | @classmethod 320 | def validate(cls, value: str) -> LnurlPayMetadata: 321 | return cls(value, json_obj=cls.__validate_metadata__(value)) 322 | 323 | @property 324 | def h(self) -> str: 325 | return sha256(self.encode("utf-8")).hexdigest() 326 | 327 | @property 328 | def text(self) -> str: 329 | output = "" 330 | 331 | for metadata in self._list: 332 | if metadata[0] == "text/plain": 333 | output = metadata[1] 334 | break 335 | 336 | return output 337 | 338 | @property 339 | def images(self) -> list[tuple[str, str]]: 340 | return [x for x in self._list if x[0].startswith("image/")] 341 | 342 | def list(self) -> list[tuple[str, str]]: 343 | return self._list 344 | 345 | 346 | class InitializationVectorBase64(ConstrainedStr): 347 | min_length = 24 348 | max_length = 24 349 | 350 | 351 | class CiphertextBase64(ConstrainedStr): 352 | min_length = 24 353 | max_length = 4096 354 | 355 | 356 | class Max144Str(ConstrainedStr): 357 | max_length = 144 358 | 359 | 360 | # LUD-04: auth base spec. 361 | class LnurlAuthActions(Enum): 362 | """Enum for auth actions""" 363 | 364 | login = "login" 365 | register = "register" 366 | link = "link" 367 | auth = "auth" 368 | 369 | 370 | class LnurlPaySuccessActionTag(Enum): 371 | """Enum for success action tags""" 372 | 373 | aes = "aes" 374 | message = "message" 375 | url = "url" 376 | 377 | 378 | class LnurlStatus(Enum): 379 | """Enum for status""" 380 | 381 | ok = "OK" 382 | error = "ERROR" 383 | 384 | 385 | class LnurlResponseTag(Enum): 386 | """Enum for response tags""" 387 | 388 | login = "login" 389 | channelRequest = "channelRequest" 390 | hostedChannelRequest = "hostedChannelRequest" 391 | payRequest = "payRequest" 392 | withdrawRequest = "withdrawRequest" 393 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pydantic import ValidationError, parse_obj_as 3 | 4 | from lnurl import ( 5 | CallbackUrl, 6 | LightningInvoice, 7 | LightningNodeUri, 8 | LnAddress, 9 | LnAddressError, 10 | Lnurl, 11 | LnurlErrorResponse, 12 | LnurlPayerData, 13 | LnurlPayerDataAuth, 14 | LnurlPayMetadata, 15 | LnurlPayResponsePayerData, 16 | LnurlPayResponsePayerDataExtra, 17 | LnurlPayResponsePayerDataOption, 18 | LnurlPayResponsePayerDataOptionAuth, 19 | ) 20 | 21 | 22 | class TestUrl: 23 | # https://github.com/pydantic/pydantic/discussions/2450 24 | @pytest.mark.parametrize( 25 | "hostport", 26 | ["service.io:443", "service.io:9000"], 27 | ) 28 | def test_parameters(self, hostport): 29 | url = parse_obj_as(CallbackUrl, f"https://{hostport}/?q=3fc3645b439ce8e7&test=ok") 30 | assert url.host == "service.io" 31 | assert url == f"https://{hostport}/?q=3fc3645b439ce8e7&test=ok" 32 | assert url.query_params == {"q": "3fc3645b439ce8e7", "test": "ok"} 33 | 34 | @pytest.mark.parametrize( 35 | "url", 36 | [ 37 | "https://service.io/?q=3fc3645b439ce8e7&test=ok", 38 | "https://[2001:db8:0:1]:80", 39 | "https://protonirockerxow.onion/", 40 | "http://protonirockerxow.onion/", 41 | "https://📙.la/⚡", # https://emojipedia.org/high-voltage-sign/ 42 | "https://xn--yt8h.la/%E2%9A%A1", 43 | "http://0.0.0.0", 44 | "http://127.0.0.1", 45 | "http://localhost", 46 | ], 47 | ) 48 | def test_valid_callback(self, url): 49 | url = parse_obj_as(CallbackUrl, url) 50 | assert isinstance(url, CallbackUrl) 51 | 52 | @pytest.mark.parametrize( 53 | "url", 54 | [ 55 | "http://service.io/?q=3fc3645b439ce8e7&test=ok", 56 | "http://[2001:db8:0:1]:80", 57 | f'https://service.io/?hash={"x" * 4096}', 58 | "https://1.1.1.1/\u0000", 59 | "http://xn--yt8h.la/%E2%9A%A1", 60 | "http://1.1.1.1", 61 | "lnurlp://service.io", 62 | ], 63 | ) 64 | def test_invalid_data_callback(self, url): 65 | with pytest.raises(ValidationError): 66 | parse_obj_as(CallbackUrl, url) 67 | 68 | @pytest.mark.parametrize( 69 | "url", 70 | [ 71 | "https://📙.la/⚡", # https://emojipedia.org/high-voltage-sign/ 72 | "https://xn--yt8h.la/%E2%9A%A1", 73 | ], 74 | ) 75 | def test_strict_rfc3986(self, monkeypatch, url): 76 | monkeypatch.setenv("LNURL_STRICT_RFC3986", "1") 77 | with pytest.raises(ValidationError): 78 | parse_obj_as(CallbackUrl, url) 79 | 80 | 81 | class TestLightningInvoice: 82 | @pytest.mark.parametrize( 83 | "bech32, hrp, prefix, amount, h", 84 | [ 85 | ( 86 | ( 87 | "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd" 88 | "5d7xmw5fk98klysy043l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf237cm2rqv9pmn5lnexfvf55" 89 | "79slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqpqlhssu04sucpnz4axcv2dstmknqq6jsk2l" 90 | ), 91 | "lntb20m", 92 | "lntb", 93 | 20, 94 | "h", 95 | ), 96 | ], 97 | ) 98 | def test_valid_invoice(self, bech32, hrp, prefix, amount, h): 99 | invoice = LightningInvoice(bech32) 100 | assert invoice == parse_obj_as(LightningInvoice, bech32) 101 | assert invoice.hrp == hrp 102 | # TODO: implement these properties 103 | # assert invoice.prefix == prefix 104 | # assert invoice.amount == amount 105 | 106 | 107 | class TestLightningNode: 108 | def test_valid_node(self): 109 | node = parse_obj_as(LightningNodeUri, "node_key@ip_address:port_number") 110 | assert node.key == "node_key" 111 | assert node.ip == "ip_address" 112 | assert node.port == "port_number" 113 | 114 | @pytest.mark.parametrize("uri", ["https://service.io/node", "node_key@ip_address", "ip_address:port_number"]) 115 | def test_invalid_node(self, uri): 116 | with pytest.raises(ValidationError): 117 | parse_obj_as(LightningNodeUri, uri) 118 | 119 | 120 | class TestLnurl: 121 | @pytest.mark.parametrize( 122 | "lightning", 123 | [ 124 | ( 125 | "LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWE3JX56NXCFK89JN2V3K" 126 | "XUCRSVTY8YMXGCMYXV6RQD3EXDSKVCTZV5CRGCN9XA3RQCMRVSCNWWRYVCYAE0UU" 127 | ), 128 | ( 129 | "lightning:LNURL1DP68GURN8GHJ7UM9WFMXJCM99E5K7TELWY7NXENRXVMRGDTZXSENJCM98PJNWE3JX56NXCFK89JN2V3K" 130 | "XUCRSVTY8YMXGCMYXV6RQD3EXDSKVCTZV5CRGCN9XA3RQCMRVSCNWWRYVCYAE0UU" 131 | ), 132 | "https://service.io?a=1&b=2", 133 | "lnurlp://service.io?a=1&b=2", 134 | "lnurlp://service.io/lnurlp?a=1&b=2", 135 | ], 136 | ) 137 | def test_valid_lnurl_and_bech32(self, lightning): 138 | lnurl = Lnurl(lightning) 139 | assert lnurl == parse_obj_as(Lnurl, lightning) 140 | if lnurl.is_lud17: 141 | assert lnurl.lud17 == lightning 142 | assert lnurl.lud17_prefix == lightning.split("://")[0] 143 | assert lnurl.is_lud17 is True 144 | assert lnurl.url == lightning.replace("lnurlp://", "https://") 145 | assert lnurl == lightning.replace("lnurlp://", "https://") 146 | else: 147 | assert lnurl.bech32 is not None 148 | assert lnurl.bech32.hrp == "lnurl" 149 | assert lnurl.bech32 == lnurl or lnurl.url == lnurl 150 | assert lnurl.lud17 is None 151 | assert lnurl.lud17_prefix is None 152 | assert lnurl.is_lud17 is False 153 | 154 | assert lnurl.is_login is False 155 | 156 | @pytest.mark.parametrize( 157 | "url", 158 | [ 159 | "lnurlp://service.io?a=1&b=2", 160 | "lnurlc://service.io", 161 | "lnurlw://service.io", 162 | "keyauth://service.io", 163 | ], 164 | ) 165 | def test_valid_lnurl_lud17(self, url: str): 166 | _lnurl = parse_obj_as(Lnurl, url) 167 | 168 | _prefix = url.split("://")[0] 169 | assert _lnurl.lud17 == url 170 | assert _lnurl.lud17_prefix == _prefix 171 | assert _lnurl.is_lud17 is True 172 | _url = url.replace("lnurlp://", "https://") 173 | _url = _url.replace("lnurlc://", "https://") 174 | _url = _url.replace("lnurlw://", "https://") 175 | _url = _url.replace("keyauth://", "https://") 176 | assert str(_lnurl.url) == _url 177 | assert str(_lnurl) == _url 178 | 179 | @pytest.mark.parametrize( 180 | "url", 181 | [ 182 | "http://service.io", 183 | "lnurlx://service.io", 184 | ], 185 | ) 186 | def test_invalid_lnurl(self, url: str): 187 | with pytest.raises(ValidationError): 188 | parse_obj_as(Lnurl, url) 189 | 190 | @pytest.mark.parametrize( 191 | "bech32", 192 | [ 193 | "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", 194 | "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", 195 | "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", 196 | ], 197 | ) 198 | def test_decode_nolnurl(self, bech32): 199 | with pytest.raises(ValidationError): 200 | parse_obj_as(Lnurl, bech32) 201 | 202 | @pytest.mark.parametrize( 203 | "url", 204 | [ 205 | "lnurlp://localhost", 206 | "http://localhost", 207 | ], 208 | ) 209 | def test_insecure_lnurl(self, url: str): 210 | lnurl = parse_obj_as(Lnurl, url) 211 | assert lnurl.url.insecure is True 212 | assert lnurl.url.host == "localhost" 213 | assert lnurl.url.startswith("http://") 214 | 215 | 216 | class TestLnurlPayMetadata: 217 | @pytest.mark.parametrize( 218 | "metadata, image_type", 219 | [ 220 | ('[["text/plain", "main text"]]', None), 221 | ('[["text/plain", "main text"], ["image/jpeg;base64", "base64encodedimage"]]', "jpeg"), 222 | ('[["text/plain", "main text"], ["image/png;base64", "base64encodedimage"]]', "png"), 223 | ('[["text/plain", "main text"], ["text/indentifier", "alan@lnbits.com"], ["text/tag", "tag"]]', None), 224 | ], 225 | ) 226 | def test_valid(self, metadata, image_type): 227 | m = parse_obj_as(LnurlPayMetadata, metadata) 228 | assert m.text == "main text" 229 | 230 | if m.images: 231 | assert len(m.images) == 1 232 | assert dict(m.images)[f"image/{image_type};base64"] == "base64encodedimage" 233 | 234 | @pytest.mark.parametrize( 235 | "metadata", 236 | [ 237 | "[]", 238 | '["text""plain"]', 239 | '[["text", "plain"]]', 240 | '[["text", "plain", "plane"]]', 241 | '[["text/plain", "main text"], ["text/plain", "two is too much"]]', 242 | '[["image/jpeg;base64", "base64encodedimage"]]', 243 | ], 244 | ) 245 | def test_invalid_data(self, metadata): 246 | with pytest.raises(ValidationError): 247 | parse_obj_as(LnurlPayMetadata, metadata) 248 | 249 | @pytest.mark.parametrize( 250 | "lnaddress", 251 | [ 252 | "donate@legend.lnbits.com", 253 | ], 254 | ) 255 | def test_valid_lnaddress(self, lnaddress): 256 | lnaddress = LnAddress(lnaddress) 257 | assert isinstance(lnaddress.url, CallbackUrl) 258 | assert lnaddress.tag is None 259 | 260 | @pytest.mark.parametrize( 261 | "lnaddress", 262 | [ 263 | "donate+lud16tag@legend.lnbits.com", 264 | ], 265 | ) 266 | def test_valid_lnaddress_with_tag(self, lnaddress): 267 | lnaddress = LnAddress(lnaddress) 268 | assert isinstance(lnaddress.url, CallbackUrl) 269 | assert lnaddress.tag == "lud16tag" 270 | 271 | @pytest.mark.parametrize( 272 | "lnaddress", 273 | [ 274 | "legend.lnbits.com", 275 | "donate@donate@legend.lnbits.com", 276 | "HELLO@lnbits.com", 277 | ], 278 | ) 279 | def test_invalid_lnaddress(self, lnaddress): 280 | with pytest.raises(LnAddressError): 281 | lnaddress = LnAddress(lnaddress) 282 | 283 | 284 | class TestPayerData: 285 | 286 | def test_valid_pay_response_payer_data(self): 287 | data = { 288 | "name": {"mandatory": True}, 289 | "pubkey": {"mandatory": True}, 290 | "auth": {"mandatory": True, "k1": "0" * 32}, 291 | "extras": [ 292 | { 293 | "name": "extra_field", 294 | "field": {"mandatory": True}, 295 | }, 296 | ], 297 | } 298 | payer_data = parse_obj_as(LnurlPayResponsePayerData, data) 299 | assert payer_data.name is not None 300 | assert payer_data.name.mandatory is True 301 | assert payer_data.pubkey is not None 302 | assert payer_data.pubkey.mandatory is True 303 | assert payer_data.auth is not None 304 | assert payer_data.auth.mandatory is True 305 | assert payer_data.auth.k1 == "0" * 32 306 | assert payer_data.extras is not None 307 | assert len(payer_data.extras) == 1 308 | assert payer_data.extras[0].name == "extra_field" 309 | assert payer_data.extras[0].field is not None 310 | 311 | def test_valid_pay_response_payer_data_models(self): 312 | data_option = LnurlPayResponsePayerDataOption( 313 | mandatory=True, 314 | ) 315 | payer_data = LnurlPayResponsePayerData( 316 | name=data_option, 317 | pubkey=data_option, 318 | auth=LnurlPayResponsePayerDataOptionAuth( 319 | mandatory=True, 320 | k1="0" * 32, 321 | ), 322 | extras=[ 323 | LnurlPayResponsePayerDataExtra( 324 | name="extra_field", 325 | field=LnurlPayResponsePayerDataOption( 326 | mandatory=True, 327 | ), 328 | ), 329 | ], 330 | ) 331 | assert payer_data.name is not None 332 | assert payer_data.name.mandatory is True 333 | assert payer_data.pubkey is not None 334 | assert payer_data.pubkey.mandatory is True 335 | assert payer_data.auth is not None 336 | assert payer_data.auth.mandatory is True 337 | assert payer_data.auth.k1 == "0" * 32 338 | assert payer_data.extras is not None 339 | assert len(payer_data.extras) == 1 340 | assert payer_data.extras[0].name == "extra_field" 341 | assert payer_data.extras[0].field is not None 342 | 343 | def test_valid_payer_data(self): 344 | data = { 345 | "name": "John Doe", 346 | "pubkey": "03a3xxxxxxxxxxxx", 347 | "auth": { 348 | "key": "key", 349 | "k1": "0" * 32, 350 | "sig": "0" * 64, 351 | }, 352 | } 353 | payer_data = parse_obj_as(LnurlPayerData, data) 354 | assert payer_data.name == "John Doe" 355 | assert payer_data.pubkey == "03a3xxxxxxxxxxxx" 356 | assert payer_data.auth is not None 357 | assert payer_data.auth.key == "key" 358 | assert payer_data.auth.k1 == "0" * 32 359 | assert payer_data.auth.sig == "0" * 64 360 | 361 | def test_valid_pay_response_payer_model(self): 362 | data = LnurlPayerData( 363 | name="John Doe", 364 | pubkey="03a3xxxxxxxxxxxx", 365 | auth=LnurlPayerDataAuth( 366 | key="key", 367 | k1="0" * 32, 368 | sig="0" * 64, 369 | ), 370 | extras={ 371 | "extra_field": "extra_value", 372 | }, 373 | ) 374 | assert data.name == "John Doe" 375 | assert data.pubkey == "03a3xxxxxxxxxxxx" 376 | assert data.auth is not None 377 | assert data.auth.key == "key" 378 | assert data.auth.k1 == "0" * 32 379 | assert data.auth.sig == "0" * 64 380 | assert data.extras is not None 381 | assert data.extras["extra_field"] == "extra_value" 382 | 383 | 384 | class TestLnurlErrorResponse: 385 | def test_error_res_details(self): 386 | res = LnurlErrorResponse(reason="detail") 387 | _dict = res.dict() 388 | assert "status" in _dict 389 | assert _dict["status"] == "ERROR" 390 | assert "reason" in _dict 391 | assert _dict["reason"] == "detail" 392 | assert res.json() == '{"status": "ERROR", "reason": "detail"}' 393 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "4.8.0" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["main"] 10 | files = [ 11 | {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, 12 | {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, 13 | ] 14 | 15 | [package.dependencies] 16 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 17 | idna = ">=2.8" 18 | sniffio = ">=1.1" 19 | typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} 20 | 21 | [package.extras] 22 | doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] 23 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] 24 | trio = ["trio (>=0.26.1)"] 25 | 26 | [[package]] 27 | name = "asn1crypto" 28 | version = "1.5.1" 29 | description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" 30 | optional = false 31 | python-versions = "*" 32 | groups = ["main"] 33 | files = [ 34 | {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, 35 | {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, 36 | ] 37 | 38 | [[package]] 39 | name = "base58" 40 | version = "2.1.1" 41 | description = "Base58 and Base58Check implementation." 42 | optional = false 43 | python-versions = ">=3.5" 44 | groups = ["main"] 45 | files = [ 46 | {file = "base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2"}, 47 | {file = "base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c"}, 48 | ] 49 | 50 | [package.extras] 51 | tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"] 52 | 53 | [[package]] 54 | name = "bech32" 55 | version = "1.2.0" 56 | description = "Reference implementation for Bech32 and segwit addresses." 57 | optional = false 58 | python-versions = ">=3.5" 59 | groups = ["main"] 60 | files = [ 61 | {file = "bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"}, 62 | {file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"}, 63 | ] 64 | 65 | [[package]] 66 | name = "bip32" 67 | version = "4.0" 68 | description = "Minimalistic implementation of the BIP32 key derivation scheme" 69 | optional = false 70 | python-versions = "*" 71 | groups = ["main"] 72 | files = [ 73 | {file = "bip32-4.0-py3-none-any.whl", hash = "sha256:9728b38336129c00e1f870bbb3e328c9632d51c1bddeef4011fd3115cb3aeff9"}, 74 | {file = "bip32-4.0.tar.gz", hash = "sha256:8035588f252f569bb414bc60df151ae431fc1c6789a19488a32890532ef3a2fc"}, 75 | ] 76 | 77 | [package.dependencies] 78 | coincurve = ">=15.0,<21" 79 | 80 | [[package]] 81 | name = "bitarray" 82 | version = "3.0.0" 83 | description = "efficient arrays of booleans -- C extension" 84 | optional = false 85 | python-versions = "*" 86 | groups = ["main"] 87 | files = [ 88 | {file = "bitarray-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ddbf71a97ad1d6252e6e93d2d703b624d0a5b77c153b12f9ea87d83e1250e0c"}, 89 | {file = "bitarray-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0e7f24a0b01e6e6a0191c50b06ca8edfdec1988d9d2b264d669d2487f4f4680"}, 90 | {file = "bitarray-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:150b7b29c36d9f1a24779aea723fdfc73d1c1c161dc0ea14990da27d4e947092"}, 91 | {file = "bitarray-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8330912be6cb8e2fbfe8eb69f82dee139d605730cadf8d50882103af9ac83bb4"}, 92 | {file = "bitarray-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e56ba8be5f17dee0ffa6d6ce85251e062ded2faa3cbd2558659c671e6c3bf96d"}, 93 | {file = "bitarray-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffd94b4803811c738e504a4b499fb2f848b2f7412d71e6b517508217c1d7929d"}, 94 | {file = "bitarray-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0255bd05ec7165e512c115423a5255a3f301417973d20a80fc5bfc3f3640bcb"}, 95 | {file = "bitarray-3.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe606e728842389943a939258809dc5db2de831b1d2e0118515059e87f7bbc1a"}, 96 | {file = "bitarray-3.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e89ea59a3ed86a6eb150d016ed28b1bedf892802d0ed32b5659d3199440f3ced"}, 97 | {file = "bitarray-3.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cf0cc2e91dd38122dec2e6541efa99aafb0a62e118179218181eff720b4b8153"}, 98 | {file = "bitarray-3.0.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2d9fe3ee51afeb909b68f97e14c6539ace3f4faa99b21012e610bbe7315c388d"}, 99 | {file = "bitarray-3.0.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:37be5482b9df3105bad00fdf7dc65244e449b130867c3879c9db1db7d72e508b"}, 100 | {file = "bitarray-3.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0027b8f3bb2bba914c79115e96a59b9924aafa1a578223a7c4f0a7242d349842"}, 101 | {file = "bitarray-3.0.0-cp310-cp310-win32.whl", hash = "sha256:628f93e9c2c23930bd1cfe21c634d6c84ec30f45f23e69aefe1fcd262186d7bb"}, 102 | {file = "bitarray-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:0b655c3110e315219e266b2732609fddb0857bc69593de29f3c2ba74b7d3f51a"}, 103 | {file = "bitarray-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:44c3e78b60070389b824d5a654afa1c893df723153c81904088d4922c3cfb6ac"}, 104 | {file = "bitarray-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:545d36332de81e4742a845a80df89530ff193213a50b4cbef937ed5a44c0e5e5"}, 105 | {file = "bitarray-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a9eb510cde3fa78c2e302bece510bf5ed494ec40e6b082dec753d6e22d5d1b1"}, 106 | {file = "bitarray-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e3727ab63dfb6bde00b281934e2212bb7529ea3006c0031a556a84d2268bea5"}, 107 | {file = "bitarray-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2055206ed653bee0b56628f6a4d248d53e5660228d355bbec0014bdfa27050ae"}, 108 | {file = "bitarray-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:147542299f458bdb177f798726e5f7d39ab8491de4182c3c6d9885ed275a3c2b"}, 109 | {file = "bitarray-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f761184b93092077c7f6b7dad7bd4e671c1620404a76620da7872ceb576a94"}, 110 | {file = "bitarray-3.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e008b7b4ce6c7f7a54b250c45c28d4243cc2a3bbfd5298fa7dac92afda229842"}, 111 | {file = "bitarray-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dfea514e665af278b2e1d4deb542de1cd4f77413bee83dd15ae16175976ea8d5"}, 112 | {file = "bitarray-3.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:66d6134b7bb737b88f1d16478ad0927c571387f6054f4afa5557825a4c1b78e2"}, 113 | {file = "bitarray-3.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3cd565253889940b4ec4768d24f101d9fe111cad4606fdb203ea16f9797cf9ed"}, 114 | {file = "bitarray-3.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4800c91a14656789d2e67d9513359e23e8a534c8ee1482bb9b517a4cfc845200"}, 115 | {file = "bitarray-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c2945e0390d1329c585c584c6b6d78be017d9c6a1288f9c92006fe907f69cc28"}, 116 | {file = "bitarray-3.0.0-cp311-cp311-win32.whl", hash = "sha256:c23286abba0cb509733c6ce8f4013cd951672c332b2e184dbefbd7331cd234c8"}, 117 | {file = "bitarray-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca79f02a98cbda1472449d440592a2fe2ad96fe55515a0447fa8864a38017cf8"}, 118 | {file = "bitarray-3.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:184972c96e1c7e691be60c3792ca1a51dd22b7f25d96ebea502fe3c9b554f25d"}, 119 | {file = "bitarray-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:787db8da5e9e29be712f7a6bce153c7bc8697ccc2c38633e347bb9c82475d5c9"}, 120 | {file = "bitarray-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2da91ab3633c66999c2a352f0ca9ae064f553e5fc0eca231d28e7e305b83e942"}, 121 | {file = "bitarray-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7edb83089acbf2c86c8002b96599071931dc4ea5e1513e08306f6f7df879a48b"}, 122 | {file = "bitarray-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996d1b83eb904589f40974538223eaed1ab0f62be8a5105c280b9bd849e685c4"}, 123 | {file = "bitarray-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4817d73d995bd2b977d9cde6050be8d407791cf1f84c8047fa0bea88c1b815bc"}, 124 | {file = "bitarray-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d47bc4ff9b0e1624d613563c6fa7b80aebe7863c56c3df5ab238bb7134e8755"}, 125 | {file = "bitarray-3.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aca0a9cd376beaccd9f504961de83e776dd209c2de5a4c78dc87a78edf61839b"}, 126 | {file = "bitarray-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:572a61fba7e3a710a8324771322fba8488d134034d349dcd036a7aef74723a80"}, 127 | {file = "bitarray-3.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a817ad70c1aff217530576b4f037dd9b539eb2926603354fcac605d824082ad1"}, 128 | {file = "bitarray-3.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2ac67b658fa5426503e9581a3fb44a26a3b346c1abd17105735f07db572195b3"}, 129 | {file = "bitarray-3.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:12f19ede03e685c5c588ab5ed63167999295ffab5e1126c5fe97d12c0718c18f"}, 130 | {file = "bitarray-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcef31b062f756ba7eebcd7890c5d5de84b9d64ee877325257bcc9782288564a"}, 131 | {file = "bitarray-3.0.0-cp312-cp312-win32.whl", hash = "sha256:656db7bdf1d81ec3b57b3cad7ec7276765964bcfd0eb81c5d1331f385298169c"}, 132 | {file = "bitarray-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f785af6b7cb07a9b1e5db0dea9ef9e3e8bb3d74874a0a61303eab9c16acc1999"}, 133 | {file = "bitarray-3.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7cb885c043000924554fe2124d13084c8fdae03aec52c4086915cd4cb87fe8be"}, 134 | {file = "bitarray-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7814c9924a0b30ecd401f02f082d8697fc5a5be3f8d407efa6e34531ff3c306a"}, 135 | {file = "bitarray-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bcf524a087b143ba736aebbb054bb399d49e77cf7c04ed24c728e411adc82bfa"}, 136 | {file = "bitarray-3.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1d5abf1d6d910599ac16afdd9a0ed3e24f3b46af57f3070cf2792f236f36e0b"}, 137 | {file = "bitarray-3.0.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9929051feeaf8d948cc0b1c9ce57748079a941a1a15c89f6014edf18adaade84"}, 138 | {file = "bitarray-3.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96cf0898f8060b2d3ae491762ae871b071212ded97ff9e1e3a5229e9fefe544c"}, 139 | {file = "bitarray-3.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab37da66a8736ad5a75a58034180e92c41e864da0152b84e71fcc253a2f69cd4"}, 140 | {file = "bitarray-3.0.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeb79e476d19b91fd6a3439853e4e5ba1b3b475920fa40d62bde719c8af786f"}, 141 | {file = "bitarray-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f75fc0198c955d840b836059bd43e0993edbf119923029ca60c4fc017cefa54a"}, 142 | {file = "bitarray-3.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f12cc7c7638074918cdcc7491aff897df921b092ffd877227892d2686e98f876"}, 143 | {file = "bitarray-3.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dbe1084935b942fab206e609fa1ed3f46ad1f2612fb4833e177e9b2a5e006c96"}, 144 | {file = "bitarray-3.0.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ac06dd72ee1e1b6e312504d06f75220b5894af1fb58f0c20643698f5122aea76"}, 145 | {file = "bitarray-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00f9a88c56e373009ac3c73c55205cfbd9683fbd247e2f9a64bae3da78795252"}, 146 | {file = "bitarray-3.0.0-cp313-cp313-win32.whl", hash = "sha256:9c6e52005e91803eb4e08c0a08a481fb55ddce97f926bae1f6fa61b3396b5b61"}, 147 | {file = "bitarray-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:cb98d5b6eac4b2cf2a5a69f60a9c499844b8bea207059e9fc45c752436e6bb49"}, 148 | {file = "bitarray-3.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:eb27c01b747649afd7e1c342961680893df6d8d81f832a6f04d8c8e03a8a54cc"}, 149 | {file = "bitarray-3.0.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4683bff52f5a0fd523fb5d3138161ef87611e63968e1fcb6cf4b0c6a86970fe0"}, 150 | {file = "bitarray-3.0.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb7302dbcfcb676f0b66f15891f091d0233c4fc23e1d4b9dc9b9e958156e347f"}, 151 | {file = "bitarray-3.0.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:153d7c416a70951dcfa73487af05d2f49c632e95602f1620cd9a651fa2033695"}, 152 | {file = "bitarray-3.0.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251cd5bd47f542893b2b61860eded54f34920ea47fd5bff038d85e7a2f7ae99b"}, 153 | {file = "bitarray-3.0.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fa4b4d9fa90124b33b251ef74e44e737021f253dc7a9174e1b39f097451f7ca"}, 154 | {file = "bitarray-3.0.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:18abdce7ab5d2104437c39670821cba0b32fdb9b2da9e6d17a4ff295362bd9dc"}, 155 | {file = "bitarray-3.0.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:2855cc01ee370f7e6e3ec97eebe44b1453c83fb35080313145e2c8c3c5243afb"}, 156 | {file = "bitarray-3.0.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:0cecaf2981c9cd2054547f651537b4f4939f9fe225d3fc2b77324b597c124e40"}, 157 | {file = "bitarray-3.0.0-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:22b00f65193fafb13aa644e16012c8b49e7d5cbb6bb72825105ff89aadaa01e3"}, 158 | {file = "bitarray-3.0.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:20f30373f0af9cb583e4122348cefde93c82865dbcbccc4997108b3d575ece84"}, 159 | {file = "bitarray-3.0.0-cp36-cp36m-win32.whl", hash = "sha256:aef404d5400d95c6ec86664df9924bde667c8865f8e33c9b7bd79823d53b3e5d"}, 160 | {file = "bitarray-3.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ec5b0f2d13da53e0975ac15ecbe8badb463bdb0bebaa09457f4df3320421915c"}, 161 | {file = "bitarray-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:041c889e69c847b8a96346650e50f728b747ae176889199c49a3f31ae1de0e23"}, 162 | {file = "bitarray-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc83ea003dd75e9ade3291ef0585577dd5524aec0c8c99305c0aaa2a7570d6db"}, 163 | {file = "bitarray-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c33129b49196aa7965ac0f16fcde7b6ad8614b606caf01669a0277cef1afe1d"}, 164 | {file = "bitarray-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ef5c787c8263c082a73219a69eb60a500e157a4ac69d1b8515ad836b0e71fb4"}, 165 | {file = "bitarray-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e15c94d79810c5ab90ddf4d943f71f14332890417be896ca253f21fa3d78d2b1"}, 166 | {file = "bitarray-3.0.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cd021ada988e73d649289cee00428b75564c46d55fbdcb0e3402e504b0ae5ea"}, 167 | {file = "bitarray-3.0.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7f1c24be7519f16a47b7e2ad1a1ef73023d34d8cbe1a3a59b185fc14baabb132"}, 168 | {file = "bitarray-3.0.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:000df24c183011b5d27c23d79970f49b6762e5bb5aacd25da9c3e9695c693222"}, 169 | {file = "bitarray-3.0.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:42bf1b222c698b467097f58b9f59dc850dfa694dde4e08237407a6a103757aa3"}, 170 | {file = "bitarray-3.0.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:648e7ce794928e8d11343b5da8ecc5b910af75a82ea1a4264d5d0a55c3785faa"}, 171 | {file = "bitarray-3.0.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:f536fc4d1a683025f9caef0bebeafd60384054579ffe0825bb9bd8c59f8c55b8"}, 172 | {file = "bitarray-3.0.0-cp37-cp37m-win32.whl", hash = "sha256:a754c1464e7b946b1cac7300c582c6fba7d66e535cd1dab76d998ad285ac5a37"}, 173 | {file = "bitarray-3.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e91d46d12781a14ccb8b284566b14933de4e3b29f8bc5e1c17de7a2001ad3b5b"}, 174 | {file = "bitarray-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:904c1d5e3bd24f0c0d37a582d2461312033c91436a6a4f3bdeeceb4bea4a899d"}, 175 | {file = "bitarray-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:47ccf9887bd595d4a0536f2310f0dcf89e17ab83b8befa7dc8727b8017120fda"}, 176 | {file = "bitarray-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:71ad0139c95c9acf4fb62e203b428f9906157b15eecf3f30dc10b55919225896"}, 177 | {file = "bitarray-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e002ac1073ac70e323a7a4bfa9ab95e7e1a85c79160799e265563f342b1557"}, 178 | {file = "bitarray-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acc07211a59e2f245e9a06f28fa374d094fb0e71cf5366eef52abbb826ddc81e"}, 179 | {file = "bitarray-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98a4070ddafabddaee70b2aa7cc6286cf73c37984169ab03af1782da2351059a"}, 180 | {file = "bitarray-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7d09ef06ba57bea646144c29764bf6b870fb3c5558ca098191e07b6a1d40bf7"}, 181 | {file = "bitarray-3.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce249ed981f428a8b61538ca82d3875847733d579dd40084ab8246549160f8a4"}, 182 | {file = "bitarray-3.0.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea40e98d751ed4b255db4a88fe8fb743374183f78470b9e9305aab186bf28ede"}, 183 | {file = "bitarray-3.0.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:928b8b6dfcd015e1a81334cfdac02815da2a2407854492a80cf8a3a922b04052"}, 184 | {file = "bitarray-3.0.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:fbb645477595ce2a0fbb678d1cfd08d3b896e5d56196d40fb9e114eeab9382b3"}, 185 | {file = "bitarray-3.0.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:dc1937a0ff2671797d35243db4b596329842480d125a65e9fe964bcffaf16dfc"}, 186 | {file = "bitarray-3.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a4f49ac31734fe654a68e2515c0da7f5bbdf2d52755ba09a42ac406f1f08c9d0"}, 187 | {file = "bitarray-3.0.0-cp38-cp38-win32.whl", hash = "sha256:6d2a2ce73f9897268f58857ad6893a1a6680c5a6b28f79d21c7d33285a5ae646"}, 188 | {file = "bitarray-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:b1047999f1797c3ea7b7c85261649249c243308dcf3632840d076d18fa72f142"}, 189 | {file = "bitarray-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:39b38a3d45dac39d528c87b700b81dfd5e8dc8e9e1a102503336310ef837c3fd"}, 190 | {file = "bitarray-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0e104f9399144fab6a892d379ba1bb4275e56272eb465059beef52a77b4e5ce6"}, 191 | {file = "bitarray-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0879f839ec8f079fa60c3255966c2e1aa7196699a234d4e5b7898fbc321901b5"}, 192 | {file = "bitarray-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9502c2230d59a4ace2fddfd770dad8e8b414cbd99517e7e56c55c20997c28b8d"}, 193 | {file = "bitarray-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57d5ef854f8ec434f2ffd9ddcefc25a10848393fe2976e2be2c8c773cf5fef42"}, 194 | {file = "bitarray-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3c36b2fcfebe15ad1c10a90c1d52a42bebe960adcbce340fef867203028fbe7"}, 195 | {file = "bitarray-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66a33a537e781eac3a352397ce6b07eedf3a8380ef4a804f8844f3f45e335544"}, 196 | {file = "bitarray-3.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa54c7e1da8cf4be0aab941ea284ec64033ede5d6de3fd47d75e77cafe986e9d"}, 197 | {file = "bitarray-3.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a667ea05ba1ea81b722682276dbef1d36990f8908cf51e570099fd505a89f931"}, 198 | {file = "bitarray-3.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d756bfeb62ca4fe65d2af7a39249d442c05070c047d03729ad6cd4c2e9b0f0bd"}, 199 | {file = "bitarray-3.0.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c9e9fef0754867d88e948ce8351c9fd7e507d8514e0f242fd67c907b9cdf98b3"}, 200 | {file = "bitarray-3.0.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:67a0b56dd02f2713f6f52cacb3f251afd67c94c5f0748026d307d87a81a8e15c"}, 201 | {file = "bitarray-3.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d8c36ddc1923bcc4c11b9994c54eaae25034812a42400b7b8a86fe6d242166a2"}, 202 | {file = "bitarray-3.0.0-cp39-cp39-win32.whl", hash = "sha256:1414a7102a3c4986f241480544f5c99f5d32258fb9b85c9c04e84e48c490ab35"}, 203 | {file = "bitarray-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:8c9733d2ff9b7838ac04bf1048baea153174753e6a47312be14c83c6a395424b"}, 204 | {file = "bitarray-3.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fef4e3b3f2084b4dae3e5316b44cda72587dcc81f68b4eb2dbda1b8d15261b61"}, 205 | {file = "bitarray-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e9eee03f187cef1e54a4545124109ee0afc84398628b4b32ebb4852b4a66393"}, 206 | {file = "bitarray-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb5702dd667f4bb10fed056ffdc4ddaae8193a52cd74cb2cdb54e71f4ef2dd1"}, 207 | {file = "bitarray-3.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:666e44b0458bb2894b64264a29f2cc7b5b2cbcc4c5e9cedfe1fdbde37a8e329a"}, 208 | {file = "bitarray-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c756a92cf1c1abf01e56a4cc40cb89f0ff9147f2a0be5b557ec436a23ff464d8"}, 209 | {file = "bitarray-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7e51e7f8289bf6bb631e1ef2a8f5e9ca287985ff518fe666abbdfdb6a848cb26"}, 210 | {file = "bitarray-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa5d8e4b28388b337face6ce4029be73585651a44866901513df44be9a491ab"}, 211 | {file = "bitarray-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3963b80a68aedcd722a9978d261ae53cb9bb6a8129cc29790f0f10ce5aca287a"}, 212 | {file = "bitarray-3.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b555006a7dea53f6bebc616a4d0249cecbf8f1fadf77860120a2e5dbdc2f167"}, 213 | {file = "bitarray-3.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4ac2027ca650a7302864ed2528220d6cc6921501b383e9917afc7a2424a1e36d"}, 214 | {file = "bitarray-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bf90aba4cff9e72e24ecdefe33bad608f147a23fa5c97790a5bab0e72fe62b6d"}, 215 | {file = "bitarray-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a199e6d7c3bad5ba9d0e4dc00dde70ee7d111c9dfc521247fa646ef59fa57e"}, 216 | {file = "bitarray-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b6c7c4f4a7b80e86e24a76f4c6b9b67d03229ea16d7d403520616535c32196"}, 217 | {file = "bitarray-3.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fc13da3518f14825b239374734fce93c1a9299ed7b558c3ec1d659ec7e4c70"}, 218 | {file = "bitarray-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:369b6d457af94af901d632c7e625ca6caf0a7484110fc91c6290ce26bc4f1478"}, 219 | {file = "bitarray-3.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ee040ad3b7dfa05e459713099f16373c1f2a6f68b43cb0575a66718e7a5daef4"}, 220 | {file = "bitarray-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dad7ba2af80f9ec1dd988c3aca7992408ec0d0b4c215b65d353d95ab0070b10"}, 221 | {file = "bitarray-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4839d3b64af51e4b8bb4a602563b98b9faeb34fd6c00ed23d7834e40a9d080fc"}, 222 | {file = "bitarray-3.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f71f24b58e75a889b9915e3197865302467f13e7390efdea5b6afc7424b3a2ea"}, 223 | {file = "bitarray-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:bcf0150ae0bcc4aa97bdfcb231b37bad1a59083c1b5012643b266012bf420e68"}, 224 | {file = "bitarray-3.0.0.tar.gz", hash = "sha256:a2083dc20f0d828a7cdf7a16b20dae56aab0f43dc4f347a3b3039f6577992b03"}, 225 | ] 226 | 227 | [[package]] 228 | name = "bitstring" 229 | version = "4.3.0" 230 | description = "Simple construction, analysis and modification of binary data." 231 | optional = false 232 | python-versions = ">=3.8" 233 | groups = ["main"] 234 | files = [ 235 | {file = "bitstring-4.3.0-py3-none-any.whl", hash = "sha256:3282a896814813f8fe5fa09dbafac842c57aace1d3bfd94546c6f1ed9aafcbe1"}, 236 | {file = "bitstring-4.3.0.tar.gz", hash = "sha256:81800bc4e00b6508716adbae648e741256355c8dfd19541f76482fb89bee0313"}, 237 | ] 238 | 239 | [package.dependencies] 240 | bitarray = ">=3.0.0,<3.1" 241 | 242 | [[package]] 243 | name = "black" 244 | version = "24.10.0" 245 | description = "The uncompromising code formatter." 246 | optional = false 247 | python-versions = ">=3.9" 248 | groups = ["dev"] 249 | files = [ 250 | {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, 251 | {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, 252 | {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, 253 | {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, 254 | {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, 255 | {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, 256 | {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, 257 | {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, 258 | {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, 259 | {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, 260 | {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, 261 | {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, 262 | {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, 263 | {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, 264 | {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, 265 | {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, 266 | {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, 267 | {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, 268 | {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, 269 | {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, 270 | {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, 271 | {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, 272 | ] 273 | 274 | [package.dependencies] 275 | click = ">=8.0.0" 276 | mypy-extensions = ">=0.4.3" 277 | packaging = ">=22.0" 278 | pathspec = ">=0.9.0" 279 | platformdirs = ">=2" 280 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 281 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 282 | 283 | [package.extras] 284 | colorama = ["colorama (>=0.4.3)"] 285 | d = ["aiohttp (>=3.10)"] 286 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 287 | uvloop = ["uvloop (>=0.15.2)"] 288 | 289 | [[package]] 290 | name = "bolt11" 291 | version = "2.1.1" 292 | description = "A library for encoding and decoding BOLT11 payment requests." 293 | optional = false 294 | python-versions = ">=3.10" 295 | groups = ["main"] 296 | files = [ 297 | {file = "bolt11-2.1.1-py3-none-any.whl", hash = "sha256:fd4edb9e73e27bf5e017f47c97f7c6827b523fcf9cab152b123961ca78323e2d"}, 298 | {file = "bolt11-2.1.1.tar.gz", hash = "sha256:4e903d77208bfc4de8fc7e183a0689ea54afe874c91d62524d3b8c09492fa7ea"}, 299 | ] 300 | 301 | [package.dependencies] 302 | base58 = "*" 303 | bech32 = "*" 304 | bitstring = "*" 305 | click = "*" 306 | coincurve = "*" 307 | 308 | [[package]] 309 | name = "certifi" 310 | version = "2025.1.31" 311 | description = "Python package for providing Mozilla's CA Bundle." 312 | optional = false 313 | python-versions = ">=3.6" 314 | groups = ["main"] 315 | files = [ 316 | {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, 317 | {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, 318 | ] 319 | 320 | [[package]] 321 | name = "cffi" 322 | version = "1.17.1" 323 | description = "Foreign Function Interface for Python calling C code." 324 | optional = false 325 | python-versions = ">=3.8" 326 | groups = ["main"] 327 | files = [ 328 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 329 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 330 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 331 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 332 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 333 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 334 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 335 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 336 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 337 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 338 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 339 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 340 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 341 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 342 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 343 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 344 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 345 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 346 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 347 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 348 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 349 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 350 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 351 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 352 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 353 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 354 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 355 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 356 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 357 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 358 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 359 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 360 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 361 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 362 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 363 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 364 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 365 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 366 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 367 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 368 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 369 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 370 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 371 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 372 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 373 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 374 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 375 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 376 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 377 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 378 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 379 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 380 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 381 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 382 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 383 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 384 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 385 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 386 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 387 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 388 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 389 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 390 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 391 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 392 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 393 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 394 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 395 | ] 396 | 397 | [package.dependencies] 398 | pycparser = "*" 399 | 400 | [[package]] 401 | name = "cfgv" 402 | version = "3.4.0" 403 | description = "Validate configuration and produce human readable error messages." 404 | optional = false 405 | python-versions = ">=3.8" 406 | groups = ["dev"] 407 | files = [ 408 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 409 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 410 | ] 411 | 412 | [[package]] 413 | name = "click" 414 | version = "8.1.8" 415 | description = "Composable command line interface toolkit" 416 | optional = false 417 | python-versions = ">=3.7" 418 | groups = ["main", "dev"] 419 | files = [ 420 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 421 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 422 | ] 423 | 424 | [package.dependencies] 425 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 426 | 427 | [[package]] 428 | name = "coincurve" 429 | version = "20.0.0" 430 | description = "Cross-platform Python CFFI bindings for libsecp256k1" 431 | optional = false 432 | python-versions = ">=3.8" 433 | groups = ["main"] 434 | files = [ 435 | {file = "coincurve-20.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d559b22828638390118cae9372a1bb6f6594f5584c311deb1de6a83163a0919b"}, 436 | {file = "coincurve-20.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33d7f6ebd90fcc550f819f7f2cce2af525c342aac07f0ccda46ad8956ad9d99b"}, 437 | {file = "coincurve-20.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22d70dd55d13fd427418eb41c20fde0a20a5e5f016e2b1bb94710701e759e7e0"}, 438 | {file = "coincurve-20.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f18d481eaae72c169f334cde1fd22011a884e0c9c6adc3fdc1fd13df8236a3"}, 439 | {file = "coincurve-20.0.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9de1ec57f43c3526bc462be58fb97910dc1fdd5acab6c71eda9f9719a5bd7489"}, 440 | {file = "coincurve-20.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a6f007c44c726b5c0b3724093c0d4fb8e294f6b6869beb02d7473b21777473a3"}, 441 | {file = "coincurve-20.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0ff1f3b81330db5092c24da2102e4fcba5094f14945b3eb40746456ceabdd6d9"}, 442 | {file = "coincurve-20.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82f7de97694d9343f26bd1c8e081b168e5f525894c12445548ce458af227f536"}, 443 | {file = "coincurve-20.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:e905b4b084b4f3b61e5a5d58ac2632fd1d07b7b13b4c6d778335a6ca1dafd7a3"}, 444 | {file = "coincurve-20.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:3657bb5ed0baf1cf8cf356e7d44aa90a7902cc3dd4a435c6d4d0bed0553ad4f7"}, 445 | {file = "coincurve-20.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44087d1126d43925bf9a2391ce5601bf30ce0dba4466c239172dc43226696018"}, 446 | {file = "coincurve-20.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccf0ba38b0f307a9b3ce28933f6c71dc12ef3a0985712ca09f48591afd597c8"}, 447 | {file = "coincurve-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:566bc5986debdf8572b6be824fd4de03d533c49f3de778e29f69017ae3fe82d8"}, 448 | {file = "coincurve-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4d70283168e146f025005c15406086513d5d35e89a60cf4326025930d45013a"}, 449 | {file = "coincurve-20.0.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:763c6122dd7d5e7a81c86414ce360dbe9a2d4afa1ca6c853ee03d63820b3d0c5"}, 450 | {file = "coincurve-20.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f00c361c356bcea386d47a191bb8ac60429f4b51c188966a201bfecaf306ff7f"}, 451 | {file = "coincurve-20.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4af57bdadd2e64d117dd0b33cfefe76e90c7a6c496a7b034fc65fd01ec249b15"}, 452 | {file = "coincurve-20.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a26437b7cbde13fb6e09261610b788ca2a0ca2195c62030afd1e1e0d1a62e035"}, 453 | {file = "coincurve-20.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ed51f8bba35e6c7676ad65539c3dbc35acf014fc402101fa24f6b0a15a74ab9e"}, 454 | {file = "coincurve-20.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:594b840fc25d74118407edbbbc754b815f1bba9759dbf4f67f1c2b78396df2d3"}, 455 | {file = "coincurve-20.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4df4416a6c0370d777aa725a25b14b04e45aa228da1251c258ff91444643f688"}, 456 | {file = "coincurve-20.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1ccc3e4db55abf3fc0e604a187fdb05f0702bc5952e503d9a75f4ae6eeb4cb3a"}, 457 | {file = "coincurve-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8335b1658a2ef5b3eb66d52647742fe8c6f413ad5b9d5310d7ea6d8060d40f"}, 458 | {file = "coincurve-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ac025e485a0229fd5394e0bf6b4a75f8a4f6cee0dcf6f0b01a2ef05c5210ff"}, 459 | {file = "coincurve-20.0.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e46e3f1c21b3330857bcb1a3a5b942f645c8bce912a8a2b252216f34acfe4195"}, 460 | {file = "coincurve-20.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:df9ff9b17a1d27271bf476cf3fa92df4c151663b11a55d8cea838b8f88d83624"}, 461 | {file = "coincurve-20.0.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4155759f071375699282e03b3d95fb473ee05c022641c077533e0d906311e57a"}, 462 | {file = "coincurve-20.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0530b9dd02fc6f6c2916716974b79bdab874227f560c422801ade290e3fc5013"}, 463 | {file = "coincurve-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:eacf9c0ce8739c84549a89c083b1f3526c8780b84517ee75d6b43d276e55f8a0"}, 464 | {file = "coincurve-20.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:52a67bfddbd6224dfa42085c88ad176559801b57d6a8bd30d92ee040de88b7b3"}, 465 | {file = "coincurve-20.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e951b1d695b62376f60519a84c4facaf756eeb9c5aff975bea0942833f185d"}, 466 | {file = "coincurve-20.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e9e548db77f4ea34c0d748dddefc698adb0ee3fab23ed19f80fb2118dac70f6"}, 467 | {file = "coincurve-20.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cdbf0da0e0809366fdfff236b7eb6e663669c7b1f46361a4c4d05f5b7e94c57"}, 468 | {file = "coincurve-20.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72222b4ecd3952e8ffcbf59bc7e0d1b181161ba170b60e5c8e1f359a43bbe7e"}, 469 | {file = "coincurve-20.0.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9add43c4807f0c17a940ce4076334c28f51d09c145cd478400e89dcfb83fb59d"}, 470 | {file = "coincurve-20.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bcc94cceea6ec8863815134083e6221a034b1ecef822d0277cf6ad2e70009b7f"}, 471 | {file = "coincurve-20.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ffbdfef6a6d147988eabaed681287a9a7e6ba45ecc0a8b94ba62ad0a7656d97"}, 472 | {file = "coincurve-20.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13335c19c7e5f36eaba2a53c68073d981980d7dc7abfee68d29f2da887ccd24e"}, 473 | {file = "coincurve-20.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:7fbfb8d16cf2bea2cf48fc5246d4cb0a06607d73bb5c57c007c9aed7509f855e"}, 474 | {file = "coincurve-20.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4870047704cddaae7f0266a549c927407c2ba0ec92d689e3d2b511736812a905"}, 475 | {file = "coincurve-20.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81ce41263517b0a9f43cd570c87720b3c13324929584fa28d2e4095969b6015d"}, 476 | {file = "coincurve-20.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:572083ccce6c7b514d482f25f394368f4ae888f478bd0b067519d33160ea2fcc"}, 477 | {file = "coincurve-20.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee5bc78a31a2f1370baf28aaff3949bc48f940a12b0359d1cd2c4115742874e6"}, 478 | {file = "coincurve-20.0.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2895d032e281c4e747947aae4bcfeef7c57eabfd9be22886c0ca4e1365c7c1f"}, 479 | {file = "coincurve-20.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d3e2f21957ada0e1742edbde117bb41758fa8691b69c8d186c23e9e522ea71cd"}, 480 | {file = "coincurve-20.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c2baa26b1aad1947ca07b3aa9e6a98940c5141c6bdd0f9b44d89e36da7282ffa"}, 481 | {file = "coincurve-20.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7eacc7944ddf9e2b7448ecbe84753841ab9874b8c332a4f5cc3b2f184db9f4a2"}, 482 | {file = "coincurve-20.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:c293c095dc690178b822cadaaeb81de3cc0d28f8bdf8216ed23551dcce153a26"}, 483 | {file = "coincurve-20.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:11a47083a0b7092d3eb50929f74ffd947c4a5e7035796b81310ea85289088c7a"}, 484 | {file = "coincurve-20.0.0.tar.gz", hash = "sha256:872419e404300302e938849b6b92a196fabdad651060b559dc310e52f8392829"}, 485 | ] 486 | 487 | [package.dependencies] 488 | asn1crypto = "*" 489 | cffi = ">=1.3.0" 490 | 491 | [package.extras] 492 | dev = ["coverage", "pytest", "pytest-benchmark"] 493 | 494 | [[package]] 495 | name = "colorama" 496 | version = "0.4.6" 497 | description = "Cross-platform colored terminal text." 498 | optional = false 499 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 500 | groups = ["main", "dev"] 501 | files = [ 502 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 503 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 504 | ] 505 | markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} 506 | 507 | [[package]] 508 | name = "coverage" 509 | version = "7.6.12" 510 | description = "Code coverage measurement for Python" 511 | optional = false 512 | python-versions = ">=3.9" 513 | groups = ["dev"] 514 | files = [ 515 | {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, 516 | {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, 517 | {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, 518 | {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, 519 | {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, 520 | {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, 521 | {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, 522 | {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, 523 | {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, 524 | {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, 525 | {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, 526 | {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, 527 | {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, 528 | {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, 529 | {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, 530 | {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, 531 | {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, 532 | {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, 533 | {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, 534 | {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, 535 | {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, 536 | {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, 537 | {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, 538 | {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, 539 | {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, 540 | {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, 541 | {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, 542 | {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, 543 | {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, 544 | {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, 545 | {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, 546 | {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, 547 | {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, 548 | {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, 549 | {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, 550 | {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, 551 | {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, 552 | {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, 553 | {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, 554 | {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, 555 | {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, 556 | {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, 557 | {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, 558 | {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, 559 | {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, 560 | {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, 561 | {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, 562 | {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, 563 | {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, 564 | {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, 565 | {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, 566 | {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, 567 | {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, 568 | {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, 569 | {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, 570 | {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, 571 | {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, 572 | {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, 573 | {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, 574 | {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, 575 | {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, 576 | {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, 577 | {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, 578 | ] 579 | 580 | [package.dependencies] 581 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 582 | 583 | [package.extras] 584 | toml = ["tomli ; python_full_version <= \"3.11.0a6\""] 585 | 586 | [[package]] 587 | name = "distlib" 588 | version = "0.3.9" 589 | description = "Distribution utilities" 590 | optional = false 591 | python-versions = "*" 592 | groups = ["dev"] 593 | files = [ 594 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 595 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 596 | ] 597 | 598 | [[package]] 599 | name = "ecdsa" 600 | version = "0.19.0" 601 | description = "ECDSA cryptographic signature library (pure python)" 602 | optional = false 603 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" 604 | groups = ["main"] 605 | files = [ 606 | {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, 607 | {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, 608 | ] 609 | 610 | [package.dependencies] 611 | six = ">=1.9.0" 612 | 613 | [package.extras] 614 | gmpy = ["gmpy"] 615 | gmpy2 = ["gmpy2"] 616 | 617 | [[package]] 618 | name = "exceptiongroup" 619 | version = "1.2.2" 620 | description = "Backport of PEP 654 (exception groups)" 621 | optional = false 622 | python-versions = ">=3.7" 623 | groups = ["main", "dev"] 624 | markers = "python_version == \"3.10\"" 625 | files = [ 626 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 627 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 628 | ] 629 | 630 | [package.extras] 631 | test = ["pytest (>=6)"] 632 | 633 | [[package]] 634 | name = "filelock" 635 | version = "3.17.0" 636 | description = "A platform independent file lock." 637 | optional = false 638 | python-versions = ">=3.9" 639 | groups = ["dev"] 640 | files = [ 641 | {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, 642 | {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, 643 | ] 644 | 645 | [package.extras] 646 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] 647 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] 648 | typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] 649 | 650 | [[package]] 651 | name = "h11" 652 | version = "0.16.0" 653 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 654 | optional = false 655 | python-versions = ">=3.8" 656 | groups = ["main"] 657 | files = [ 658 | {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, 659 | {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, 660 | ] 661 | 662 | [[package]] 663 | name = "httpcore" 664 | version = "1.0.9" 665 | description = "A minimal low-level HTTP client." 666 | optional = false 667 | python-versions = ">=3.8" 668 | groups = ["main"] 669 | files = [ 670 | {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, 671 | {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, 672 | ] 673 | 674 | [package.dependencies] 675 | certifi = "*" 676 | h11 = ">=0.16" 677 | 678 | [package.extras] 679 | asyncio = ["anyio (>=4.0,<5.0)"] 680 | http2 = ["h2 (>=3,<5)"] 681 | socks = ["socksio (==1.*)"] 682 | trio = ["trio (>=0.22.0,<1.0)"] 683 | 684 | [[package]] 685 | name = "httpx" 686 | version = "0.28.1" 687 | description = "The next generation HTTP client." 688 | optional = false 689 | python-versions = ">=3.8" 690 | groups = ["main"] 691 | files = [ 692 | {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, 693 | {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, 694 | ] 695 | 696 | [package.dependencies] 697 | anyio = "*" 698 | certifi = "*" 699 | httpcore = "==1.*" 700 | idna = "*" 701 | 702 | [package.extras] 703 | brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] 704 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 705 | http2 = ["h2 (>=3,<5)"] 706 | socks = ["socksio (==1.*)"] 707 | zstd = ["zstandard (>=0.18.0)"] 708 | 709 | [[package]] 710 | name = "identify" 711 | version = "2.6.9" 712 | description = "File identification library for Python" 713 | optional = false 714 | python-versions = ">=3.9" 715 | groups = ["dev"] 716 | files = [ 717 | {file = "identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150"}, 718 | {file = "identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf"}, 719 | ] 720 | 721 | [package.extras] 722 | license = ["ukkonen"] 723 | 724 | [[package]] 725 | name = "idna" 726 | version = "3.10" 727 | description = "Internationalized Domain Names in Applications (IDNA)" 728 | optional = false 729 | python-versions = ">=3.6" 730 | groups = ["main"] 731 | files = [ 732 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 733 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 734 | ] 735 | 736 | [package.extras] 737 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 738 | 739 | [[package]] 740 | name = "iniconfig" 741 | version = "2.0.0" 742 | description = "brain-dead simple config-ini parsing" 743 | optional = false 744 | python-versions = ">=3.7" 745 | groups = ["dev"] 746 | files = [ 747 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 748 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 749 | ] 750 | 751 | [[package]] 752 | name = "mypy" 753 | version = "1.17.1" 754 | description = "Optional static typing for Python" 755 | optional = false 756 | python-versions = ">=3.9" 757 | groups = ["dev"] 758 | files = [ 759 | {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, 760 | {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, 761 | {file = "mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df"}, 762 | {file = "mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390"}, 763 | {file = "mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94"}, 764 | {file = "mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b"}, 765 | {file = "mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58"}, 766 | {file = "mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5"}, 767 | {file = "mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd"}, 768 | {file = "mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b"}, 769 | {file = "mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5"}, 770 | {file = "mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b"}, 771 | {file = "mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb"}, 772 | {file = "mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403"}, 773 | {file = "mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056"}, 774 | {file = "mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341"}, 775 | {file = "mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb"}, 776 | {file = "mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19"}, 777 | {file = "mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7"}, 778 | {file = "mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81"}, 779 | {file = "mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6"}, 780 | {file = "mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849"}, 781 | {file = "mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14"}, 782 | {file = "mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a"}, 783 | {file = "mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733"}, 784 | {file = "mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd"}, 785 | {file = "mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0"}, 786 | {file = "mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a"}, 787 | {file = "mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91"}, 788 | {file = "mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed"}, 789 | {file = "mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9"}, 790 | {file = "mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99"}, 791 | {file = "mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8"}, 792 | {file = "mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8"}, 793 | {file = "mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259"}, 794 | {file = "mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d"}, 795 | {file = "mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9"}, 796 | {file = "mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01"}, 797 | ] 798 | 799 | [package.dependencies] 800 | mypy_extensions = ">=1.0.0" 801 | pathspec = ">=0.9.0" 802 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 803 | typing_extensions = ">=4.6.0" 804 | 805 | [package.extras] 806 | dmypy = ["psutil (>=4.0)"] 807 | faster-cache = ["orjson"] 808 | install-types = ["pip"] 809 | mypyc = ["setuptools (>=50)"] 810 | reports = ["lxml"] 811 | 812 | [[package]] 813 | name = "mypy-extensions" 814 | version = "1.0.0" 815 | description = "Type system extensions for programs checked with the mypy type checker." 816 | optional = false 817 | python-versions = ">=3.5" 818 | groups = ["dev"] 819 | files = [ 820 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 821 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 822 | ] 823 | 824 | [[package]] 825 | name = "nodeenv" 826 | version = "1.9.1" 827 | description = "Node.js virtual environment builder" 828 | optional = false 829 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 830 | groups = ["dev"] 831 | files = [ 832 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 833 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 834 | ] 835 | 836 | [[package]] 837 | name = "packaging" 838 | version = "24.2" 839 | description = "Core utilities for Python packages" 840 | optional = false 841 | python-versions = ">=3.8" 842 | groups = ["dev"] 843 | files = [ 844 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 845 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 846 | ] 847 | 848 | [[package]] 849 | name = "pathspec" 850 | version = "0.12.1" 851 | description = "Utility library for gitignore style pattern matching of file paths." 852 | optional = false 853 | python-versions = ">=3.8" 854 | groups = ["dev"] 855 | files = [ 856 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 857 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 858 | ] 859 | 860 | [[package]] 861 | name = "platformdirs" 862 | version = "4.3.6" 863 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 864 | optional = false 865 | python-versions = ">=3.8" 866 | groups = ["dev"] 867 | files = [ 868 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 869 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 870 | ] 871 | 872 | [package.extras] 873 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 874 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 875 | type = ["mypy (>=1.11.2)"] 876 | 877 | [[package]] 878 | name = "pluggy" 879 | version = "1.5.0" 880 | description = "plugin and hook calling mechanisms for python" 881 | optional = false 882 | python-versions = ">=3.8" 883 | groups = ["dev"] 884 | files = [ 885 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 886 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 887 | ] 888 | 889 | [package.extras] 890 | dev = ["pre-commit", "tox"] 891 | testing = ["pytest", "pytest-benchmark"] 892 | 893 | [[package]] 894 | name = "pre-commit" 895 | version = "3.8.0" 896 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 897 | optional = false 898 | python-versions = ">=3.9" 899 | groups = ["dev"] 900 | files = [ 901 | {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, 902 | {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, 903 | ] 904 | 905 | [package.dependencies] 906 | cfgv = ">=2.0.0" 907 | identify = ">=1.0.0" 908 | nodeenv = ">=0.11.1" 909 | pyyaml = ">=5.1" 910 | virtualenv = ">=20.10.0" 911 | 912 | [[package]] 913 | name = "pycparser" 914 | version = "2.22" 915 | description = "C parser in Python" 916 | optional = false 917 | python-versions = ">=3.8" 918 | groups = ["main"] 919 | files = [ 920 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 921 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 922 | ] 923 | 924 | [[package]] 925 | name = "pycryptodomex" 926 | version = "3.21.0" 927 | description = "Cryptographic library for Python" 928 | optional = false 929 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" 930 | groups = ["main"] 931 | files = [ 932 | {file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"}, 933 | {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"}, 934 | {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"}, 935 | {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"}, 936 | {file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"}, 937 | {file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"}, 938 | {file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"}, 939 | {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"}, 940 | {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"}, 941 | {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"}, 942 | {file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"}, 943 | {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"}, 944 | {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"}, 945 | {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"}, 946 | {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"}, 947 | {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"}, 948 | {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"}, 949 | {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"}, 950 | {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"}, 951 | {file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"}, 952 | {file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"}, 953 | {file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"}, 954 | {file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"}, 955 | {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"}, 956 | {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"}, 957 | {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"}, 958 | {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"}, 959 | {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"}, 960 | {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"}, 961 | {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"}, 962 | {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"}, 963 | {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, 964 | ] 965 | 966 | [[package]] 967 | name = "pydantic" 968 | version = "1.10.21" 969 | description = "Data validation and settings management using python type hints" 970 | optional = false 971 | python-versions = ">=3.7" 972 | groups = ["main"] 973 | files = [ 974 | {file = "pydantic-1.10.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:245e486e0fec53ec2366df9cf1cba36e0bbf066af7cd9c974bbbd9ba10e1e586"}, 975 | {file = "pydantic-1.10.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c54f8d4c151c1de784c5b93dfbb872067e3414619e10e21e695f7bb84d1d1fd"}, 976 | {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b64708009cfabd9c2211295144ff455ec7ceb4c4fb45a07a804309598f36187"}, 977 | {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a148410fa0e971ba333358d11a6dea7b48e063de127c2b09ece9d1c1137dde4"}, 978 | {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:36ceadef055af06e7756eb4b871cdc9e5a27bdc06a45c820cd94b443de019bbf"}, 979 | {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0501e1d12df6ab1211b8cad52d2f7b2cd81f8e8e776d39aa5e71e2998d0379f"}, 980 | {file = "pydantic-1.10.21-cp310-cp310-win_amd64.whl", hash = "sha256:c261127c275d7bce50b26b26c7d8427dcb5c4803e840e913f8d9df3f99dca55f"}, 981 | {file = "pydantic-1.10.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b6350b68566bb6b164fb06a3772e878887f3c857c46c0c534788081cb48adf4"}, 982 | {file = "pydantic-1.10.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:935b19fdcde236f4fbf691959fa5c3e2b6951fff132964e869e57c70f2ad1ba3"}, 983 | {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6a04efdcd25486b27f24c1648d5adc1633ad8b4506d0e96e5367f075ed2e0b"}, 984 | {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ba253eb5af8d89864073e6ce8e6c8dec5f49920cff61f38f5c3383e38b1c9f"}, 985 | {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:57f0101e6c97b411f287a0b7cf5ebc4e5d3b18254bf926f45a11615d29475793"}, 986 | {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e85834f0370d737c77a386ce505c21b06bfe7086c1c568b70e15a568d9670d"}, 987 | {file = "pydantic-1.10.21-cp311-cp311-win_amd64.whl", hash = "sha256:6a497bc66b3374b7d105763d1d3de76d949287bf28969bff4656206ab8a53aa9"}, 988 | {file = "pydantic-1.10.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ed4a5f13cf160d64aa331ab9017af81f3481cd9fd0e49f1d707b57fe1b9f3ae"}, 989 | {file = "pydantic-1.10.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b7693bb6ed3fbe250e222f9415abb73111bb09b73ab90d2d4d53f6390e0ccc1"}, 990 | {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185d5f1dff1fead51766da9b2de4f3dc3b8fca39e59383c273f34a6ae254e3e2"}, 991 | {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38e6d35cf7cd1727822c79e324fa0677e1a08c88a34f56695101f5ad4d5e20e5"}, 992 | {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1d7c332685eafacb64a1a7645b409a166eb7537f23142d26895746f628a3149b"}, 993 | {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c9b782db6f993a36092480eeaab8ba0609f786041b01f39c7c52252bda6d85f"}, 994 | {file = "pydantic-1.10.21-cp312-cp312-win_amd64.whl", hash = "sha256:7ce64d23d4e71d9698492479505674c5c5b92cda02b07c91dfc13633b2eef805"}, 995 | {file = "pydantic-1.10.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0067935d35044950be781933ab91b9a708eaff124bf860fa2f70aeb1c4be7212"}, 996 | {file = "pydantic-1.10.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5e8148c2ce4894ce7e5a4925d9d3fdce429fb0e821b5a8783573f3611933a251"}, 997 | {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4973232c98b9b44c78b1233693e5e1938add5af18042f031737e1214455f9b8"}, 998 | {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:662bf5ce3c9b1cef32a32a2f4debe00d2f4839fefbebe1d6956e681122a9c839"}, 999 | {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98737c3ab5a2f8a85f2326eebcd214510f898881a290a7939a45ec294743c875"}, 1000 | {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0bb58bbe65a43483d49f66b6c8474424d551a3fbe8a7796c42da314bac712738"}, 1001 | {file = "pydantic-1.10.21-cp313-cp313-win_amd64.whl", hash = "sha256:e622314542fb48542c09c7bd1ac51d71c5632dd3c92dc82ede6da233f55f4848"}, 1002 | {file = "pydantic-1.10.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d356aa5b18ef5a24d8081f5c5beb67c0a2a6ff2a953ee38d65a2aa96526b274f"}, 1003 | {file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08caa8c0468172d27c669abfe9e7d96a8b1655ec0833753e117061febaaadef5"}, 1004 | {file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c677aa39ec737fec932feb68e4a2abe142682f2885558402602cd9746a1c92e8"}, 1005 | {file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:79577cc045d3442c4e845df53df9f9202546e2ba54954c057d253fc17cd16cb1"}, 1006 | {file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:b6b73ab347284719f818acb14f7cd80696c6fdf1bd34feee1955d7a72d2e64ce"}, 1007 | {file = "pydantic-1.10.21-cp37-cp37m-win_amd64.whl", hash = "sha256:46cffa24891b06269e12f7e1ec50b73f0c9ab4ce71c2caa4ccf1fb36845e1ff7"}, 1008 | {file = "pydantic-1.10.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:298d6f765e3c9825dfa78f24c1efd29af91c3ab1b763e1fd26ae4d9e1749e5c8"}, 1009 | {file = "pydantic-1.10.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2f4a2305f15eff68f874766d982114ac89468f1c2c0b97640e719cf1a078374"}, 1010 | {file = "pydantic-1.10.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35b263b60c519354afb3a60107d20470dd5250b3ce54c08753f6975c406d949b"}, 1011 | {file = "pydantic-1.10.21-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e23a97a6c2f2db88995496db9387cd1727acdacc85835ba8619dce826c0b11a6"}, 1012 | {file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:3c96fed246ccc1acb2df032ff642459e4ae18b315ecbab4d95c95cfa292e8517"}, 1013 | {file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b92893ebefc0151474f682e7debb6ab38552ce56a90e39a8834734c81f37c8a9"}, 1014 | {file = "pydantic-1.10.21-cp38-cp38-win_amd64.whl", hash = "sha256:b8460bc256bf0de821839aea6794bb38a4c0fbd48f949ea51093f6edce0be459"}, 1015 | {file = "pydantic-1.10.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d387940f0f1a0adb3c44481aa379122d06df8486cc8f652a7b3b0caf08435f7"}, 1016 | {file = "pydantic-1.10.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:266ecfc384861d7b0b9c214788ddff75a2ea123aa756bcca6b2a1175edeca0fe"}, 1017 | {file = "pydantic-1.10.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61da798c05a06a362a2f8c5e3ff0341743e2818d0f530eaac0d6898f1b187f1f"}, 1018 | {file = "pydantic-1.10.21-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a621742da75ce272d64ea57bd7651ee2a115fa67c0f11d66d9dcfc18c2f1b106"}, 1019 | {file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9e3e4000cd54ef455694b8be9111ea20f66a686fc155feda1ecacf2322b115da"}, 1020 | {file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f198c8206640f4c0ef5a76b779241efb1380a300d88b1bce9bfe95a6362e674d"}, 1021 | {file = "pydantic-1.10.21-cp39-cp39-win_amd64.whl", hash = "sha256:e7f0cda108b36a30c8fc882e4fc5b7eec8ef584aa43aa43694c6a7b274fb2b56"}, 1022 | {file = "pydantic-1.10.21-py3-none-any.whl", hash = "sha256:db70c920cba9d05c69ad4a9e7f8e9e83011abb2c6490e561de9ae24aee44925c"}, 1023 | {file = "pydantic-1.10.21.tar.gz", hash = "sha256:64b48e2b609a6c22178a56c408ee1215a7206077ecb8a193e2fda31858b2362a"}, 1024 | ] 1025 | 1026 | [package.dependencies] 1027 | typing-extensions = ">=4.2.0" 1028 | 1029 | [package.extras] 1030 | dotenv = ["python-dotenv (>=0.10.4)"] 1031 | email = ["email-validator (>=1.0.3)"] 1032 | 1033 | [[package]] 1034 | name = "pytest" 1035 | version = "7.4.4" 1036 | description = "pytest: simple powerful testing with Python" 1037 | optional = false 1038 | python-versions = ">=3.7" 1039 | groups = ["dev"] 1040 | files = [ 1041 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 1042 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 1043 | ] 1044 | 1045 | [package.dependencies] 1046 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 1047 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 1048 | iniconfig = "*" 1049 | packaging = "*" 1050 | pluggy = ">=0.12,<2.0" 1051 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 1052 | 1053 | [package.extras] 1054 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 1055 | 1056 | [[package]] 1057 | name = "pytest-asyncio" 1058 | version = "0.23.8" 1059 | description = "Pytest support for asyncio" 1060 | optional = false 1061 | python-versions = ">=3.8" 1062 | groups = ["dev"] 1063 | files = [ 1064 | {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, 1065 | {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, 1066 | ] 1067 | 1068 | [package.dependencies] 1069 | pytest = ">=7.0.0,<9" 1070 | 1071 | [package.extras] 1072 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 1073 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 1074 | 1075 | [[package]] 1076 | name = "pytest-cov" 1077 | version = "4.1.0" 1078 | description = "Pytest plugin for measuring coverage." 1079 | optional = false 1080 | python-versions = ">=3.7" 1081 | groups = ["dev"] 1082 | files = [ 1083 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 1084 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 1085 | ] 1086 | 1087 | [package.dependencies] 1088 | coverage = {version = ">=5.2.1", extras = ["toml"]} 1089 | pytest = ">=4.6" 1090 | 1091 | [package.extras] 1092 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 1093 | 1094 | [[package]] 1095 | name = "pyyaml" 1096 | version = "6.0.2" 1097 | description = "YAML parser and emitter for Python" 1098 | optional = false 1099 | python-versions = ">=3.8" 1100 | groups = ["dev"] 1101 | files = [ 1102 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 1103 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 1104 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 1105 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 1106 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 1107 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 1108 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 1109 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 1110 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 1111 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 1112 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 1113 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 1114 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 1115 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 1116 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 1117 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 1118 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 1119 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 1120 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 1121 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 1122 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 1123 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 1124 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 1125 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 1126 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 1127 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 1128 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 1129 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 1130 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 1131 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 1132 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 1133 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 1134 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 1135 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 1136 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 1137 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 1138 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 1139 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 1140 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 1141 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 1142 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 1143 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 1144 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 1145 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 1146 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 1147 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 1148 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 1149 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 1150 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 1151 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 1152 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 1153 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 1154 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "ruff" 1159 | version = "0.12.3" 1160 | description = "An extremely fast Python linter and code formatter, written in Rust." 1161 | optional = false 1162 | python-versions = ">=3.7" 1163 | groups = ["dev"] 1164 | files = [ 1165 | {file = "ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2"}, 1166 | {file = "ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041"}, 1167 | {file = "ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882"}, 1168 | {file = "ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901"}, 1169 | {file = "ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0"}, 1170 | {file = "ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6"}, 1171 | {file = "ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc"}, 1172 | {file = "ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687"}, 1173 | {file = "ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e"}, 1174 | {file = "ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311"}, 1175 | {file = "ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07"}, 1176 | {file = "ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12"}, 1177 | {file = "ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b"}, 1178 | {file = "ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f"}, 1179 | {file = "ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d"}, 1180 | {file = "ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7"}, 1181 | {file = "ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1"}, 1182 | {file = "ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77"}, 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "six" 1187 | version = "1.17.0" 1188 | description = "Python 2 and 3 compatibility utilities" 1189 | optional = false 1190 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1191 | groups = ["main"] 1192 | files = [ 1193 | {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, 1194 | {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, 1195 | ] 1196 | 1197 | [[package]] 1198 | name = "sniffio" 1199 | version = "1.3.1" 1200 | description = "Sniff out which async library your code is running under" 1201 | optional = false 1202 | python-versions = ">=3.7" 1203 | groups = ["main"] 1204 | files = [ 1205 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 1206 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "tomli" 1211 | version = "2.2.1" 1212 | description = "A lil' TOML parser" 1213 | optional = false 1214 | python-versions = ">=3.8" 1215 | groups = ["dev"] 1216 | markers = "python_full_version <= \"3.11.0a6\"" 1217 | files = [ 1218 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 1219 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 1220 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 1221 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 1222 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 1223 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 1224 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 1225 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 1226 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 1227 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 1228 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 1229 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 1230 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 1231 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 1232 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 1233 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 1234 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 1235 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 1236 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 1237 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 1238 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 1239 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 1240 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 1241 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 1242 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 1243 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 1244 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 1245 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 1246 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 1247 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 1248 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 1249 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 1250 | ] 1251 | 1252 | [[package]] 1253 | name = "types-requests" 1254 | version = "2.32.0.20250306" 1255 | description = "Typing stubs for requests" 1256 | optional = false 1257 | python-versions = ">=3.9" 1258 | groups = ["dev"] 1259 | files = [ 1260 | {file = "types_requests-2.32.0.20250306-py3-none-any.whl", hash = "sha256:25f2cbb5c8710b2022f8bbee7b2b66f319ef14aeea2f35d80f18c9dbf3b60a0b"}, 1261 | {file = "types_requests-2.32.0.20250306.tar.gz", hash = "sha256:0962352694ec5b2f95fda877ee60a159abdf84a0fc6fdace599f20acb41a03d1"}, 1262 | ] 1263 | 1264 | [package.dependencies] 1265 | urllib3 = ">=2" 1266 | 1267 | [[package]] 1268 | name = "typing-extensions" 1269 | version = "4.12.2" 1270 | description = "Backported and Experimental Type Hints for Python 3.8+" 1271 | optional = false 1272 | python-versions = ">=3.8" 1273 | groups = ["main", "dev"] 1274 | files = [ 1275 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 1276 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "urllib3" 1281 | version = "2.5.0" 1282 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1283 | optional = false 1284 | python-versions = ">=3.9" 1285 | groups = ["dev"] 1286 | files = [ 1287 | {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, 1288 | {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, 1289 | ] 1290 | 1291 | [package.extras] 1292 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 1293 | h2 = ["h2 (>=4,<5)"] 1294 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1295 | zstd = ["zstandard (>=0.18.0)"] 1296 | 1297 | [[package]] 1298 | name = "virtualenv" 1299 | version = "20.29.3" 1300 | description = "Virtual Python Environment builder" 1301 | optional = false 1302 | python-versions = ">=3.8" 1303 | groups = ["dev"] 1304 | files = [ 1305 | {file = "virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170"}, 1306 | {file = "virtualenv-20.29.3.tar.gz", hash = "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac"}, 1307 | ] 1308 | 1309 | [package.dependencies] 1310 | distlib = ">=0.3.7,<1" 1311 | filelock = ">=3.12.2,<4" 1312 | platformdirs = ">=3.9.1,<5" 1313 | 1314 | [package.extras] 1315 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1316 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] 1317 | 1318 | [metadata] 1319 | lock-version = "2.1" 1320 | python-versions = ">=3.10" 1321 | content-hash = "6fa2ecfe0e7f236e040a5f5e44a5c750ee1e2bca4aea499dc6c0f4ebf92f5437" 1322 | --------------------------------------------------------------------------------