diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e1d6a008..0259c5df 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -12,14 +12,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.8 + - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.12' - name: Install dependencies run: | python -m pip install --upgrade pip poetry - poetry config installer.modern-installation false poetry install - name: Docs run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 697c1cff..7210d2fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - uniswap-version: [1, 2, 3, 4] + uniswap-version: [2, 3, 4] network: ["mainnet"] # include: # - network: arbitrum @@ -40,12 +40,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.8' - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: '20' - + python-version: '3.12' # Set up poetry cache, from https://github.com/python-poetry/poetry/blob/45a9b8f20384591d0a33ae876bcf23656f928ec0/.github/workflows/main.yml - name: Get full python version id: full-python-version @@ -56,7 +51,6 @@ jobs: run: | python -m pip install --upgrade pip poetry poetry config virtualenvs.in-project true - poetry config installer.modern-installation false - name: Set up cache uses: actions/cache@v4 @@ -72,7 +66,6 @@ jobs: - name: Install dependencies run: | poetry install - npm install -g ganache@7.5.0 - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 @@ -100,7 +93,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.8' + python-version: '3.12' # Set up poetry cache, from https://github.com/python-poetry/poetry/blob/45a9b8f20384591d0a33ae876bcf23656f928ec0/.github/workflows/main.yml - name: Get full python version @@ -113,7 +106,6 @@ jobs: run: | python -m pip install --upgrade pip poetry poetry config virtualenvs.in-project true - poetry config installer.modern-installation false - name: Set up cache uses: actions/cache@v4 diff --git a/Makefile b/Makefile index 38f8f0b5..ec5ec57e 100644 --- a/Makefile +++ b/Makefile @@ -7,10 +7,10 @@ typecheck: poetry run mypy --pretty lint: - poetry run flake8 + poetry run ruff check uniswap format: - black uniswap + poetry run ruff format uniswap format-abis: npx prettier --write --parser=json uniswap/assets/*/*.abi diff --git a/README.md b/README.md index 6772c4ee..07fa4452 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,6 @@ The unofficial Python client for [Uniswap](https://uniswap.io/). Documentation is available at https://uniswap-python.com/ -**Want to help implement support for Uniswap v4?** See [issue #337](https://github.com/uniswap-python/uniswap-python/issues/337) - ## Functionality * A simple to use Python wrapper for all available contract functions and variables @@ -30,6 +28,9 @@ Documentation is available at https://uniswap-python.com/ ### Supports + - Uniswap v4 (as of v0.8.0, beta) + - Swaps, price quoting, liquidity management + - Pool discovery via pool cache service - Uniswap v3 (as of v0.5.0) - Including beta support for Arbitrum & Optimism deployments (as of v0.5.4) - Uniswap v2 (as of v0.4.0) @@ -47,7 +48,13 @@ See our [Getting started guide](https://uniswap-python.com/getting-started.html) Unit tests are under development using the pytest framework. Contributions are welcome! -Test are run on a fork of the main net using ganache-cli. You need to install it with `npm install -g ganache-cli` before running tests. +Tests run on a fork of mainnet using [Anvil](https://getfoundry.sh) (part of Foundry). Install Foundry with: + +```sh +curl -L https://foundry.paradigm.xyz | bash +export PATH="$PATH:$HOME/.foundry/bin" +foundryup +``` To run the full test suite, in the project directory set the `PROVIDER` env variable to a mainnet provider, and run: @@ -59,9 +66,13 @@ make test poetry run pytest --capture=no # doesn't capture output (verbose) ``` -## Support our continued work! +## Funding + +Development of this library has been funded by: -You can support us on [Gitcoin Grants](https://gitcoin.co/grants/2631/uniswap-python). +* The **[Uniswap Grants Program](https://www.uniswapfoundation.org/grants)**, which awarded a $15k grant (2021) that funded the development of Uniswap v3 support. +* **Community donations** through [Gitcoin Grants](https://gitcoin.co/grants/2631/uniswap-python), which were used to compensate contributors (see [#181](https://github.com/uniswap-python/uniswap-python/discussions/181)). +* **[Superuser Labs](https://superuserlabs.org/)**, Erik's company, which has sponsored continued development and maintenance, including funding [@liquid-8](https://github.com/liquid-8)'s work on Uniswap v4 support. ## Authors @@ -70,8 +81,6 @@ You can support us on [Gitcoin Grants](https://gitcoin.co/grants/2631/uniswap-py * [@liquid-8](https://github.com/liquid-8) * ...and [others](https://github.com/uniswap-python/uniswap-python/graphs/contributors) -*Want to help out with development? We have funding to those that do! See [#181](https://github.com/uniswap-python/uniswap-python/discussions/181)* - Contributors also earn this beautiful [GitPOAP](https://gitpoap.notion.site/What-s-a-GitPOAP-5b085daac4b4429994b5231be028b3d9) for their contributions! @@ -161,7 +170,7 @@ _A huge thank you [Erik Bjäreholt](https://github.com/ErikBjare) for adding Uni * Switched from setup.py to pyproject.toml/poetry * Switched from Travis to GitHub Actions * For CI to work in your repo, you need to set the secret MAINNET_PROVIDER. I use Infura. -* Running tests on a local fork of mainnet using ganache-cli (started as a fixture) +* Running tests on a local fork of mainnet using Anvil/Foundry (started as a fixture) * Fixed tests for make_trade and make_trade_output * Added type annotations to the entire codebase and check them with mypy in CI * Formatted entire codebase with black diff --git a/docs/conf.py b/docs/conf.py index f839250c..a831965b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] extlinks = { - "issue": ("https://github.com/shanefontaine/uniswap-python/issues/%s", "issue #"), + "issue": ("https://github.com/shanefontaine/uniswap-python/issues/%s", "issue #%s"), } diff --git a/docs/examples.rst b/docs/examples.rst index 746f6556..021ff0ce 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -1,6 +1,196 @@ Examples ======== -No examples here yet! Why don't you contribute some? +This page shows common usage patterns for Uniswap v2 and v3. For v4 examples, +see the dedicated :doc:`v4` guide. -In the meantime, see the :ref:`Getting started` guide. +The code snippets here mirror the `test suite +`_, which +runs every example against a live mainnet fork using +`Anvil `_. + +.. contents:: Table of contents + :local: + :depth: 2 + +Uniswap v2 +---------- + +Initialization +`````````````` + +.. code:: python + + from uniswap import Uniswap + + ETH = "0x0000000000000000000000000000000000000000" + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" + + uni = Uniswap( + address="0xYOUR_ADDRESS", + private_key="0xYOUR_PRIVATE_KEY", # or None for read-only + version=2, + provider="https://mainnet.infura.io/v3/YOUR_PROJECT_ID", + ) + +Getting prices +`````````````` + +.. code:: python + + ONE_ETH = 10**18 + + # How much USDC do I get for 1 ETH? + usdc_out = uni.get_price_input(ETH, USDC, ONE_ETH) + print(f"1 ETH → {usdc_out / 10**6:.2f} USDC") + + # How much ETH do I need to buy exactly 1000 USDC? + eth_needed = uni.get_price_output(ETH, USDC, 1000 * 10**6) + print(f"ETH needed for 1000 USDC: {eth_needed / ONE_ETH:.4f}") + +Making swaps +```````````` + +.. code:: python + + # Sell 0.1 ETH, receive USDC (exact input) + tx = uni.make_trade(ETH, USDC, ONE_ETH // 10) + + # Buy exactly 100 USDC, pay in ETH (exact output) + tx = uni.make_trade_output(ETH, USDC, 100 * 10**6) + + # Sell ETH → DAI with a custom recipient + tx = uni.make_trade(ETH, DAI, ONE_ETH // 10, recipient="0xSOME_OTHER_ADDRESS") + +Multi-hop swaps +``````````````` + +For pairs without a direct v2 pool, route through an intermediate token: + +.. code:: python + + WBTC = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + + # ETH → WBTC (routed automatically through WETH by the v2 router) + wbtc_out = uni.get_price_input(ETH, WBTC, ONE_ETH // 10) + tx = uni.make_trade(ETH, WBTC, ONE_ETH // 10) + +Uniswap v3 +---------- + +v3 adds concentrated liquidity pools at multiple fee tiers. Always specify +``fee`` to select the pool — the right tier depends on the pair's volatility. + +Initialization +`````````````` + +.. code:: python + + from uniswap import Uniswap + + ETH = "0x0000000000000000000000000000000000000000" + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + WBTC = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" + + uni = Uniswap( + address="0xYOUR_ADDRESS", + private_key="0xYOUR_PRIVATE_KEY", + version=3, + provider="https://mainnet.infura.io/v3/YOUR_PROJECT_ID", + ) + +Fee tiers +````````` + +Common v3 fee tiers: + +- ``500`` — 0.05% (stablecoin pairs, e.g. USDC/USDT) +- ``3000`` — 0.30% (most pairs, e.g. ETH/USDC) +- ``10000`` — 1.00% (exotic/volatile pairs) + +Getting prices +`````````````` + +.. code:: python + + ONE_ETH = 10**18 + + # Quote using the 0.30% ETH/USDC pool + usdc_out = uni.get_price_input(ETH, USDC, ONE_ETH, fee=3000) + print(f"1 ETH → {usdc_out / 10**6:.2f} USDC (0.30% pool)") + + # Compare with the 0.05% pool (better rate for large trades) + usdc_out_low = uni.get_price_input(ETH, USDC, ONE_ETH, fee=500) + print(f"1 ETH → {usdc_out_low / 10**6:.2f} USDC (0.05% pool)") + + # Exact output quote + eth_needed = uni.get_price_output(ETH, USDC, 1000 * 10**6, fee=500) + +Making swaps +```````````` + +.. code:: python + + # Sell 0.1 ETH for USDC via the 0.05% pool + tx = uni.make_trade(ETH, USDC, ONE_ETH // 10, fee=500) + + # Buy exactly 100 USDC, paying in ETH + tx = uni.make_trade_output(ETH, USDC, 100 * 10**6, fee=500) + +Multi-hop swaps +``````````````` + +The v3 client does not expose a multi-hop path parameter in ``make_trade``. +For pairs without a direct pool, execute two single-hop trades in sequence. +Wait for the first transaction and use the wallet's confirmed balance increase, +not its quote, as the second hop's input: + +.. code:: python + + DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" + + # ETH → USDC (first hop, 0.05% pool) + usdc_before = uni.get_token_balance(USDC) + tx1 = uni.make_trade(ETH, USDC, ONE_ETH // 10, fee=500) + uni.w3.eth.wait_for_transaction_receipt(tx1) + usdc_received = uni.get_token_balance(USDC) - usdc_before + + # USDC → DAI (second hop, 0.01% stable pool) + tx2 = uni.make_trade(USDC, DAI, usdc_received, fee=100) + +Liquidity management (v3) +````````````````````````` + +.. code:: python + + from uniswap.util import default_tick_range + + ETH = "0x0000000000000000000000000000000000000000" + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + ONE_ETH = 10**18 + + # Get the pool contract instance + pool = uni.get_pool_instance(ETH, USDC, fee=500) + + # Sensible full-range tick bounds for this fee tier + tick_lower, tick_upper = default_tick_range(fee=500) + + # Mint a liquidity position (returns TxReceipt) + receipt = uni.mint_liquidity( + pool, + amount0=ONE_ETH // 10, + amount1=340 * 10**6, + tick_lower=tick_lower, + tick_upper=tick_upper, + deadline=2**64, + ) + assert receipt["status"] + + # Get your token IDs (ERC-721 NFTs representing positions) + positions = uni.get_liquidity_positions() + token_id = positions[0] + + # Close the position (collects fees + withdraws liquidity in one call) + receipt = uni.close_position(token_id, deadline=2**64) diff --git a/docs/index.rst b/docs/index.rst index a64479f3..76dfb2fe 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,8 +28,9 @@ A good place to start is the :doc:`getting-started` guide. :caption: Contents: getting-started + v4 api - cli + cli examples supported-deployments diff --git a/docs/v4.rst b/docs/v4.rst new file mode 100644 index 00000000..25937594 --- /dev/null +++ b/docs/v4.rst @@ -0,0 +1,409 @@ +Uniswap v4 +========== + +Uniswap v4 introduces a fundamentally different architecture from v2 and v3. +All pools live inside a single **PoolManager** singleton; positions are ERC-721 +tokens managed by the **PositionManager**; and every pool is uniquely identified +by a :class:`~uniswap.types.PoolKey` struct instead of a standalone contract address. + +.. contents:: Table of contents + :local: + :depth: 2 + +The PoolKey +----------- + +A :class:`~uniswap.types.PoolKey` is a five-field tuple that uniquely identifies a pool: + +.. code:: python + + from uniswap.types import PoolKey + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + eth_usdc_pool = PoolKey( + currency0=ETH_ADDRESS, # "0x000...000" — ETH is the zero address in v4 + currency1="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # USDC + fee=500, # 0.05% fee tier + tick_spacing=10, # tick spacing that matches the fee tier + hooks=ZERO_HOOK, # "0x000...000" means no hook contract + ) + +.. note:: + + In Uniswap v4, native ETH is represented by the **zero address** + (``0x000...000``), not WETH. The two currencies in a pool are always ordered + so that ``currency0 < currency1`` (lexicographic address ordering). + +Common fee tiers and their tick spacings: + ++------------+--------+--------------+ +| ``fee`` | Rate | Tick spacing | ++============+========+==============+ +| ``100`` | 0.01% | 1 | ++------------+--------+--------------+ +| ``500`` | 0.05% | 10 | ++------------+--------+--------------+ +| ``3000`` | 0.30% | 60 | ++------------+--------+--------------+ +| ``10000`` | 1.00% | 200 | ++------------+--------+--------------+ + +Connecting to Uniswap v4 +------------------------ + +Import :class:`~uniswap.Uniswap4` and provide a wallet address, private key, and +a mainnet Web3 provider. Omit ``private_key`` for read-only use. + +.. code:: python + + from uniswap import Uniswap4 + + address = "0xYOUR_ADDRESS" + private_key = "0xYOUR_PRIVATE_KEY" # or None for read-only + + uni = Uniswap4( + address=address, + private_key=private_key, + provider="https://mainnet.infura.io/v3/YOUR_PROJECT_ID", + ) + + # Or pass a Web3 instance directly: + from web3 import Web3 + w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID")) + uni = Uniswap4(address=address, private_key=private_key, web3=w3) + +The ``PROVIDER`` environment variable is also accepted: + +.. code:: sh + + export PROVIDER="https://mainnet.infura.io/v3/YOUR_PROJECT_ID" + +Getting the spot price +---------------------- + +:func:`~uniswap.Uniswap4.get_token_token_spot_price` returns the current price +of ``token0`` denominated in ``token1``, derived from the pool's ``sqrtPriceX96`` +slot via the v4 StateView contract. + +.. code:: python + + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + + # How many USDC per 1 ETH? + price = uni.get_token_token_spot_price( + token0=ETH_ADDRESS, + token1=USDC, + fee=500, + tick_spacing=10, + hooks=ZERO_HOOK, + ) + print(f"ETH/USDC spot price: {price:.2f}") # e.g. 3412.54 + + # Inverse: ETH price in USDC terms + price_inv = uni.get_token_token_spot_price(USDC, ETH_ADDRESS, fee=500, tick_spacing=10) + +Quoting a trade +--------------- + +The quote functions call the on-chain v4 Quoter contract and account for +liquidity depth and fees. Use them instead of the spot price whenever you need +an accurate estimate for a specific trade size. + +Exact input — how much output do I get? +``````````````````````````````````````` + +:func:`~uniswap.Uniswap4.get_price_input` returns the amount of ``token1`` +received for a fixed amount of ``token0``. + +.. code:: python + + from uniswap.constants import ETH_ADDRESS + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + ONE_ETH = 10**18 + + # How much USDC do I get for 1 ETH? + usdc_out = uni.get_price_input( + token0=ETH_ADDRESS, + token1=USDC, + qty=ONE_ETH, + fee=500, + tick_spacing=10, + ) + print(f"1 ETH → {usdc_out / 10**6:.2f} USDC") + +For a 2-hop route (e.g. ETH → USDC → USDT), pass a ``route`` list of +:class:`~uniswap.types.PoolKey` objects instead of ``fee``/``tick_spacing``: + +.. code:: python + + from uniswap.types import PoolKey + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7" + + eth_usdc = PoolKey(currency0=ETH_ADDRESS, currency1=USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK) + usdc_usdt = PoolKey(currency0=USDC, currency1=USDT, fee=100, tick_spacing=1, hooks=ZERO_HOOK) + + usdt_out = uni.get_price_input( + token0=ETH_ADDRESS, + token1=USDT, + qty=ONE_ETH, + route=[eth_usdc, usdc_usdt], + ) + +Exact output — how much input do I need? +````````````````````````````````````````` + +:func:`~uniswap.Uniswap4.get_price_output` returns the amount of ``token0`` +required to receive a fixed amount of ``token1``. + +.. code:: python + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + + # How much ETH do I need to buy exactly 1000 USDC? + eth_needed = uni.get_price_output( + token0=ETH_ADDRESS, + token1=USDC, + qty=1000 * 10**6, # 1000 USDC in smallest unit + fee=500, + tick_spacing=10, + ) + print(f"ETH needed for 1000 USDC: {eth_needed / 10**18:.4f}") + +Estimating price impact +----------------------- + +:func:`~uniswap.Uniswap4.estimate_price_impact` compares the quoted price to +the pool spot price to estimate slippage. + +.. code:: python + + ONE_ETH = 10**18 + + impact = uni.estimate_price_impact( + token0=ETH_ADDRESS, + token1=USDC, + qty=ONE_ETH, + fee=500, + tick_spacing=10, + ) + print(f"Price impact: {impact:.4%}") # e.g. "Price impact: 0.0152%" + + if impact > 0.01: # warn above 1% + print("High price impact — consider splitting the trade.") + +Making a swap +------------- + +.. warning:: + + Always verify the expected output (via :func:`~uniswap.Uniswap4.get_price_input` + or :func:`~uniswap.Uniswap4.estimate_price_impact`) before executing a swap. + Low-liquidity pools can cause large, unexpected losses. + +:func:`~uniswap.Uniswap4.make_swap_input` executes a swap for a fixed input +amount. Pass the quoted output as ``qtycap``; the client applies its configured +``max_slippage`` tolerance when it builds the transaction. + +.. code:: python + + from uniswap.types import PoolKey + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + ONE_ETH = 10**18 + SELL_AMOUNT = ONE_ETH // 10 # 0.1 ETH + + pool_key = PoolKey( + currency0=ETH_ADDRESS, + currency1=USDC, + fee=500, + tick_spacing=10, + hooks=ZERO_HOOK, + ) + + # Uniswap4 applies its configured max_slippage (1% by default) to this quote + expected_usdc = uni.get_price_input(ETH_ADDRESS, USDC, SELL_AMOUNT, fee=500, tick_spacing=10) + + tx_hash = uni.make_swap_input( + input_token=ETH_ADDRESS, + output_token=USDC, + qty=SELL_AMOUNT, + qtycap=expected_usdc, + swap_pool_key=pool_key, + ) + receipt = uni.w3.eth.wait_for_transaction_receipt(tx_hash) + print(f"Swap mined in block {receipt['blockNumber']}") + +:func:`~uniswap.Uniswap4.make_swap_output` buys an exact output amount. Pass the +quoted input as ``qtycap``; the client applies ``max_slippage`` to derive the +maximum input: + +.. code:: python + + # Buy exactly 100 USDC; max_slippage is applied to this quoted ETH input + expected_eth = uni.get_price_output(ETH_ADDRESS, USDC, 100 * 10**6, fee=500, tick_spacing=10) + + tx_hash = uni.make_swap_output( + input_token=ETH_ADDRESS, + output_token=USDC, + qty=100 * 10**6, + qtycap=expected_eth, + swap_pool_key=pool_key, + ) + +Discovering pools +----------------- + +:class:`~uniswap.util.V4pools` scans ``PoolManager Initialize`` events to find +all pools for a given pair of tokens. + +.. code:: python + + from web3 import Web3 + from uniswap.util import V4pools + from uniswap.constants import ETH_ADDRESS + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + + w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID")) + pools = V4pools(w3) + + # Scan the chain (slow on first run — save to disk for reuse) + pools.fetch_poolkey_data(chunk_size=2000) + pools.save_poolkeys_list("/tmp/v4_pools.json") + + # Load from disk on subsequent runs + pools.load_poolkeys_list("/tmp/v4_pools.json") + + # Get all ETH/USDC pools + eth_usdc_pools = pools.get_poolkeys_sublist(ETH_ADDRESS, USDC) + for pk in eth_usdc_pools: + print(f"fee={pk.fee}, tick_spacing={pk.tick_spacing}, hooks={pk.hooks}") + +Managing liquidity +------------------ + +Uniswap v4 positions are ERC-721 tokens managed by the PositionManager. + +Creating a pool +``````````````` + +New pools are created via :func:`~uniswap.Uniswap4.create_pool`, which sets the +initial price. The price is expressed as ``sqrtPriceX96`` (an integer). + +.. code:: python + + import math + from uniswap.types import PoolKey + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + pool_key = PoolKey( + currency0=ETH_ADDRESS, currency1=USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK + ) + + # Encode an initial price of ~3400 USDC per ETH as sqrtPriceX96 + # price_ratio = token1_amount / token0_amount (in smallest units) + price_ratio = (3400 * 10**6) / 10**18 + sqrt_price_x96 = int(math.sqrt(price_ratio) * 2**96) + + tx = uni.create_pool(pool_key=pool_key, sqrt_price_x96=sqrt_price_x96) + uni.w3.eth.wait_for_transaction_receipt(tx) + +Minting a position +`````````````````` + +:func:`~uniswap.Uniswap4.mint_position` adds liquidity to a tick range and +returns an ERC-721 NFT representing the position. + +.. code:: python + + ONE_ETH = 10**18 + + tx = uni.mint_position( + pool_key=pool_key, + tick_lower=-887270, # near minimum tick + tick_upper=887270, # near maximum tick (full range) + liquidity=10**18, # amount of liquidity to provide + amount0=ONE_ETH // 10, # max ETH to spend + amount1=340 * 10**6, # max USDC to spend + ) + + # Retrieve the ERC-721 token ID(s) minted by this transaction + token_ids = uni.get_minted_token_id(tx.hex()) + token_id = token_ids[0] + print(f"Minted position with token ID: {token_id}") + +Reading position state +`````````````````````` + +.. code:: python + + from uniswap.types import PoolKey + + position = uni.get_position_info(token_id) + liquidity = uni.position_manager_get_position_liquidity(token_id) + print(f"Liquidity: {liquidity}") + + # Read the position value (amounts of token0/token1 at current price) + ETH_DECIMALS = 18 + USDC_DECIMALS = 6 + value = uni.get_position_value(token_id, ETH_DECIMALS, USDC_DECIMALS) + print(f"token0 amount: {value['amount0']}, token1 amount: {value['amount1']}") + +Collecting fees and removing liquidity +``````````````````````````````````````` + +.. code:: python + + from uniswap.types import PoolKey + + # Build the PoolKey from position info + position = uni.get_position_info(token_id) + pool_key = PoolKey( + position["currency0"], position["currency1"], + position["fee"], position["tickSpacing"], position["hooks"] + ) + + # Collect all accumulated fees + uni.collect_fees(pool_key=pool_key, token_id=token_id) + + # Remove all liquidity (burn requires the position to be fully empty) + liquidity = uni.position_manager_get_position_liquidity(token_id) + uni.decrease_liquidity( + pool_key=pool_key, + token_id=token_id, + liquidity=liquidity, + amount0_min=0, + amount1_min=0, + ) + + # Burn the position NFT once fully emptied + uni.burn_position(pool_key=pool_key, token_id=token_id, amount0_min=0, amount1_min=0) + +Reading pool state via StateView +--------------------------------- + +The StateView contract gives read-only access to pool state without making a +swap. + +.. code:: python + + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + + # Current pool price and tick + slot0 = uni.stateview_get_slot0(ETH_ADDRESS, USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK) + print(f"sqrtPriceX96: {slot0['sqrtPriceX96']}") + print(f"current tick: {slot0['tick']}") + + # Total in-range liquidity + liquidity = uni.stateview_get_liquidity(ETH_ADDRESS, USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK) + print(f"in-range liquidity: {liquidity}") diff --git a/examples/price_impact.py b/examples/price_impact.py index e9c99c99..1a0cdff9 100644 --- a/examples/price_impact.py +++ b/examples/price_impact.py @@ -1,14 +1,12 @@ -from typing import List - from web3 import Web3 from uniswap import Uniswap from uniswap.types import AddressLike -eth = Web3.toChecksumAddress("0x0000000000000000000000000000000000000000") -weth = Web3.toChecksumAddress("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") -usdt = Web3.toChecksumAddress("0xdac17f958d2ee523a2206206994597c13d831ec7") -vxv = Web3.toChecksumAddress("0x7d29a64504629172a429e64183d6673b9dacbfce") +eth = Web3.to_checksum_address("0x0000000000000000000000000000000000000000") +weth = Web3.to_checksum_address("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") +usdt = Web3.to_checksum_address("0xdac17f958d2ee523a2206206994597c13d831ec7") +vxv = Web3.to_checksum_address("0x7d29a64504629172a429e64183d6673b9dacbfce") def _perc(f: float) -> str: @@ -23,7 +21,7 @@ def usdt_to_vxv_v2(): """ uniswap = Uniswap(address=None, private_key=None, version=2) - route: List[AddressLike] = [usdt, weth, vxv] + route: list[AddressLike] = [usdt, weth, vxv] # Compare the results with the output of: # https://app.uniswap.org/#/swap?use=v2&inputCurrency=0xdac17f958d2ee523a2206206994597c13d831ec7&outputCurrency=0x7d29a64504629172a429e64183d6673b9dacbfce @@ -32,14 +30,14 @@ def usdt_to_vxv_v2(): # price = uniswap.get_price_input(usdt, vxv, qty, route=route) / 10 ** 18 # print(price) - impact = uniswap.estimate_price_impact(usdt, vxv, qty, route=route) + impact = uniswap.estimate_price_impact(usdt, vxv, qty, fee=3000, route=route) # NOTE: Not sure why this differs from the quote in the UI? # Getting -27% in the UI for 10 USDT, but this returns >95% # The slippage for v3 (in example below) returns correct results. print(f"Impact for buying VXV on v2 with {qty / 10**8} USDT: {_perc(impact)}") qty = 13900 * 10**8 - impact = uniswap.estimate_price_impact(usdt, vxv, qty, route=route) + impact = uniswap.estimate_price_impact(usdt, vxv, qty, fee=3000, route=route) print(f"Impact for buying VXV on v2 with {qty / 10**8} USDT: {_perc(impact)}") diff --git a/examples/v4_examples.py b/examples/v4_examples.py index bcc9f714..8f907c80 100644 --- a/examples/v4_examples.py +++ b/examples/v4_examples.py @@ -13,7 +13,7 @@ def pool_tests(): first_block = 21688329 print("Testing fetch_poolkey_data()...") try: - v4pools_test.fetch_poolkey_data(first_block, chunk_size=500, clear_list=False) + v4pools_test.fetch_poolkey_data(first_block, chunk_size=5, clear_list=False) print("Test passed.") except Exception as e: print(f"Test failed. {e}") @@ -90,9 +90,9 @@ def quoter_tests(): test_pool2_tick_spacing, default_test_hooks, ) - test_path_1hop = list() + test_path_1hop: list = [] test_path_1hop.append(test_pool_key1) - test_path_2hop = list() + test_path_2hop: list = [] test_path_2hop.append(test_pool_key1) test_path_2hop.append(test_pool_key2) @@ -592,9 +592,9 @@ def swap_tests(): test_pool2_tick_spacing, default_test_hooks, ) - test_path_1hop = list() + test_path_1hop: list = [] test_path_1hop.append(test_pool_key1) - test_path_2hop = list() + test_path_2hop: list = [] test_path_2hop.append(test_pool_key1) test_path_2hop.append(test_pool_key2) @@ -923,7 +923,7 @@ def reserves_lens_tests(): print() print() - # pool_tests() + pool_tests() quoter_tests() price_impact_tests() state_view_tests() diff --git a/poetry.lock b/poetry.lock index d2d3d4f4..6439b71e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -15,182 +15,198 @@ files = [ [package.dependencies] pygments = ">=1.5" +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472"}, + {file = "aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d"}, +] + [[package]] name = "aiohttp" -version = "3.8.4" +version = "3.14.3" description = "Async http client/server framework (asyncio)" optional = false -python-versions = ">=3.6" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1"}, - {file = "aiohttp-3.8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a"}, - {file = "aiohttp-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5"}, - {file = "aiohttp-3.8.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949"}, - {file = "aiohttp-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea"}, - {file = "aiohttp-3.8.4-cp310-cp310-win32.whl", hash = "sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1"}, - {file = "aiohttp-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4"}, - {file = "aiohttp-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05"}, - {file = "aiohttp-3.8.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b"}, - {file = "aiohttp-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24"}, - {file = "aiohttp-3.8.4-cp311-cp311-win32.whl", hash = "sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d"}, - {file = "aiohttp-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc"}, - {file = "aiohttp-3.8.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01"}, - {file = "aiohttp-3.8.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71"}, - {file = "aiohttp-3.8.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff"}, - {file = "aiohttp-3.8.4-cp36-cp36m-win32.whl", hash = "sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777"}, - {file = "aiohttp-3.8.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e"}, - {file = "aiohttp-3.8.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab"}, - {file = "aiohttp-3.8.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6"}, - {file = "aiohttp-3.8.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241"}, - {file = "aiohttp-3.8.4-cp37-cp37m-win32.whl", hash = "sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a"}, - {file = "aiohttp-3.8.4-cp37-cp37m-win_amd64.whl", hash = "sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15"}, - {file = "aiohttp-3.8.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8"}, - {file = "aiohttp-3.8.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275"}, - {file = "aiohttp-3.8.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d"}, - {file = "aiohttp-3.8.4-cp38-cp38-win32.whl", hash = "sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54"}, - {file = "aiohttp-3.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567"}, - {file = "aiohttp-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2"}, - {file = "aiohttp-3.8.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14"}, - {file = "aiohttp-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4"}, - {file = "aiohttp-3.8.4-cp39-cp39-win32.whl", hash = "sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a"}, - {file = "aiohttp-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04"}, - {file = "aiohttp-3.8.4.tar.gz", hash = "sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32"}, + {file = "aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7"}, + {file = "aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19"}, + {file = "aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71"}, + {file = "aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf"}, + {file = "aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7"}, + {file = "aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"}, ] [package.dependencies] -aiosignal = ">=1.1.2" -async-timeout = ">=4.0.0a3,<5.0" -asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""} +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<4.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} -yarl = ">=1.0,<2.0" +propcache = ">=0.2.0" +typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} +yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns", "cchardet ; python_version < \"3.10\""] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\" and sys_platform != \"android\" and sys_platform != \"ios\"", "aiodns (>=3.3.0) ; sys_platform != \"android\" and sys_platform != \"ios\"", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\" and sys_platform != \"android\" and sys_platform != \"ios\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" -version = "1.3.1" +version = "1.4.0" description = "aiosignal: a list of registered asynchronous callbacks" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, ] [package.dependencies] frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "alabaster" -version = "0.7.13" -description = "A configurable sidebar-enabled Sphinx theme" +version = "1.0.0" +description = "A light, configurable Sphinx theme" optional = false -python-versions = ">=3.6" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, - {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] [[package]] -name = "async-timeout" -version = "4.0.2" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, -] - -[package.dependencies] -typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} - -[[package]] -name = "asynctest" -version = "0.13.0" -description = "Enhance the standard unittest package with features for testing asyncio libraries" +name = "annotated-types" +version = "0.8.0" +description = "Reusable constraint types to use with typing.Annotated" optional = false -python-versions = ">=3.5" +python-versions = ">=3.10" groups = ["main"] -markers = "python_version < \"3.8\"" -files = [ - {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, - {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, -] - -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["dev"] -markers = "sys_platform == \"win32\"" files = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] [[package]] @@ -199,7 +215,7 @@ version = "22.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.6" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, @@ -214,18 +230,18 @@ tests-no-zope = ["cloudpickle ; platform_python_implementation == \"CPython\"", [[package]] name = "babel" -version = "2.12.1" +version = "2.18.0" description = "Internationalization utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, - {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, ] -[package.dependencies] -pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} +[package.extras] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "beautifulsoup4" @@ -343,80 +359,16 @@ files = [ {file = "bitarray-2.7.3.tar.gz", hash = "sha256:f71256a32609b036adad932e1228b66a6b4e2cae6be397e588ddc0babd9a78b9"}, ] -[[package]] -name = "black" -version = "23.3.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "cached-property" -version = "1.5.2" -description = "A decorator for caching properties in classes." -optional = false -python-versions = "*" -groups = ["main"] -markers = "python_version < \"3.8\"" -files = [ - {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, - {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, -] - [[package]] name = "certifi" -version = "2022.12.7" +version = "2026.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, ] [[package]] @@ -518,97 +470,69 @@ files = [ [[package]] name = "ckzg" -version = "1.0.2" +version = "2.1.8" description = "Python bindings for C-KZG-4844" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "ckzg-1.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdd082bc0f2a595e3546658ecbe1ff78fe65b0ab7e619a8197a62d94f46b5b46"}, - {file = "ckzg-1.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50ca4af4e2f1a1e8b0a7e97b3aef39dedbb0d52d90866ece424f13f8df1b5972"}, - {file = "ckzg-1.0.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e9dc671b0a307ea65d0a216ca496c272dd3c1ed890ddc2a306da49b0d8ffc83"}, - {file = "ckzg-1.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d95e97a0d0f7758119bb905fb5688222b1556de465035614883c42fe4a047d1f"}, - {file = "ckzg-1.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27261672154cbd477d84d289845b0022fbdbe2ba45b7a2a2051c345fa04c8334"}, - {file = "ckzg-1.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c16d5ee1ddbbbad0367ff970b3ec9f6d1879e9f928023beda59ae9e16ad99e4c"}, - {file = "ckzg-1.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:09043738b029bdf4fdc82041b395cfc6f5b5cf63435e5d4d685d24fd14c834d3"}, - {file = "ckzg-1.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3c0afa232d2312e3101aaddb6971b486b0038a0f9171500bc23143f5749eff55"}, - {file = "ckzg-1.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:96e8281b6d58cf91b9559e1bd38132161d63467500838753364c68e825df2e2c"}, - {file = "ckzg-1.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b874167de1d6de72890a2ad5bd9aa7adbddc41c3409923b59cf4ef27f83f79da"}, - {file = "ckzg-1.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d2ccd68b0743e20e853e31a08da490a8d38c7f12b9a0c4ee63ef5afa0dc2427"}, - {file = "ckzg-1.0.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e8d534ddbe785c44cf1cd62ee32d78b4310d66dd70e42851f5468af655b81f5"}, - {file = "ckzg-1.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c732cda00c76b326f39ae97edfc6773dd231b7c77288b38282584a7aee77c3a7"}, - {file = "ckzg-1.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abc5a27284db479ead4c053ff086d6e222914f1b0aa08b80eabfa116dbed4f7a"}, - {file = "ckzg-1.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6bd5006cb3e802744309450183087a6594d50554814eee19065f7064dff7b05"}, - {file = "ckzg-1.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3594470134eda7adf2813ad3f1da55ced98c8a393262f47ce3890c5afa05b23e"}, - {file = "ckzg-1.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fea56f39e48b60c1ff6f751c47489e353d1bd95cae65c429cf5f87735d794431"}, - {file = "ckzg-1.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:f769eb2e1056ca396462460079f6849c778f58884bb24b638ff7028dd2120b65"}, - {file = "ckzg-1.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e3cb2f8c767aee57e88944f90848e8689ce43993b9ff21589cfb97a562208fe7"}, - {file = "ckzg-1.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b29889f5bc5db530f766871c0ff4133e7270ecf63aaa3ca756d3b2731980802"}, - {file = "ckzg-1.0.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfcc70fb76b3d36125d646110d5001f2aa89c1c09ff5537a4550cdb7951f44d4"}, - {file = "ckzg-1.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ca8a256cdd56d06bc5ef24caac64845240dbabca402c5a1966d519b2514b4ec"}, - {file = "ckzg-1.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ea91b0236384f93ad1df01d530672f09e254bd8c3cf097ebf486aebb97f6c8c"}, - {file = "ckzg-1.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:65311e72780105f239d1d66512629a9f468b7c9f2609b8567fc68963ac638ef9"}, - {file = "ckzg-1.0.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0d7600ce7a73ac41d348712d0c1fe5e4cb6caa329377064cfa3a6fd8fbffb410"}, - {file = "ckzg-1.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19893ee7bd7da8688382cb134cb9ee7bce5c38e3a9386e3ed99bb010487d2d17"}, - {file = "ckzg-1.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:c3e1a9a72695e777497e95bb2213316a1138f82d1bb5d67b9c029a522d24908e"}, - {file = "ckzg-1.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2f59da9cb82b6a4be615f2561a255731eededa7ecd6ba4b2f2dedfc918ef137"}, - {file = "ckzg-1.0.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c915e1f2ef51657c3255d8b1e2aea6e0b93348ae316b2b79eaadfb17ad8f514e"}, - {file = "ckzg-1.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcc0d2031fcabc4be37e9e602c926ef9347238d2f58c1b07e0c147f60b9e760b"}, - {file = "ckzg-1.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cdaad2745425d7708e76e8e56a52fdaf5c5cc1cfefd5129d24ff8dbe06a012d"}, - {file = "ckzg-1.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1ec775649daade1b93041aac9c1660c2ad9828b57ccd2eeb5a3074d8f05e544a"}, - {file = "ckzg-1.0.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:02f9cc3e38b3702ec5895a1ebf927fd02b8f5c2f93c7cb9e438581b5b74472c8"}, - {file = "ckzg-1.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0e816af31951b5e94e6bc069f21fe783427c190526e0437e16c4488a34ddcacc"}, - {file = "ckzg-1.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:651ba33ee2d7fefff14ca519a72996b733402f8b043fbfef12d5fe2a442d86d8"}, - {file = "ckzg-1.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:489763ad92e2175fb6ab455411f03ec104c630470d483e11578bf2e00608f283"}, - {file = "ckzg-1.0.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69e1376284e9a5094d7c4d3e552202d6b32a67c5acc461b0b35718d8ec5c7363"}, - {file = "ckzg-1.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb9d0b09ca1bdb5955b626d6645f811424ae0fcab47699a1a938a3ce0438c25f"}, - {file = "ckzg-1.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87a121ace8feb6c9386f247e7e36ef55e584fc8a6b1bc2c60757a59c1efe364"}, - {file = "ckzg-1.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:97c27153fab853f017fed159333b27beeb2e0da834c92c9ecdc26d0e5c3983b3"}, - {file = "ckzg-1.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b26799907257c39471cb3665f66f7630797140131606085c2c94a7094ab6ddf2"}, - {file = "ckzg-1.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:283a40c625222560fda3dcb912b666f7d50f9502587b73c4358979f519f1c961"}, - {file = "ckzg-1.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5f029822d27c52b9c3dbe5706408b099da779f10929be0422a09a34aa026a872"}, - {file = "ckzg-1.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edaea8fb50b01c6c19768d9305ad365639a8cd804754277d5108dcae4808f00b"}, - {file = "ckzg-1.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27be65c88d5d773a30e6f198719cefede7e25cad807384c3d65a09c11616fc9d"}, - {file = "ckzg-1.0.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9ac729c5c6f3d2c030c0bc8c9e10edc253e36f002cfe227292035009965d349"}, - {file = "ckzg-1.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1528bc2b95aac6d184a90b023602c40d7b11b577235848c1b5593c00cf51d37"}, - {file = "ckzg-1.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:071dc7fc179316ce1bfabaa056156e4e84f312c4560ab7b9529a3b9a84019df3"}, - {file = "ckzg-1.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:895044069de7010be6c7ee703f03fd7548267a0823cf60b9dd26ec50267dd9e8"}, - {file = "ckzg-1.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ed8c99cd3d9af596470e0481fd58931007288951719bad026f0dd486dd0ec11"}, - {file = "ckzg-1.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:74d87eafe561d4bfb544a4f3419d26c56ad7de00f39789ef0fdb09515544d12e"}, - {file = "ckzg-1.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:54d71e5ca416bd51c543f9f51e426e6792f8a0280b83aef92faad1b826f401ea"}, - {file = "ckzg-1.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da2d9988781a09a4577ee7ea8f51fe4a94b4422789a523164f5ba3118566ad41"}, - {file = "ckzg-1.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d9e030af7d6acdcb356fddfb095048bc8e880fe4cd70ff2206c64f33bf384a0d"}, - {file = "ckzg-1.0.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:145ae31c3d499d1950567bd636dc5b24292b600296b9deb5523bc20d8f7b51c3"}, - {file = "ckzg-1.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d81e68e84d80084da298471ad5eaddfcc1cf73545cb24e9453550c8186870982"}, - {file = "ckzg-1.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67064bbbeba1a6892c9c80b3d0c2a540ff48a5ca5356fdb2a8d998b264e43e6"}, - {file = "ckzg-1.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:99694917eb6decefc0d330d9887a89ea770824b2fa76eb830bab5fe57ea5c20c"}, - {file = "ckzg-1.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fca227ce0ce3427254a113fdb3aed5ecd99c1fc670cb0c60cc8a2154793678e4"}, - {file = "ckzg-1.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a66a690d3d1801085d11de6825df47a99b465ff32dbe90be4a3c9f43c577da96"}, - {file = "ckzg-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:272adfe471380d10e4a0e1639d877e504555079a60233dd82249c799b15be81e"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f37be0054ebb4b8ac6e6d5267290b239b09e7ddc611776051b4c3c4032d161ba"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:611c03a170f0f746180eeb0cc28cdc6f954561b8eb9013605a046de86520ee6b"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75b2f0ab341f3c33702ce64e1c101116c7462a25686d0b1a0193ca654ad4f96e"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab29fc61fbd32096b82b02e6b18ae0d7423048d3540b7b90805b16ae10bdb769"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e43741e7453262aa3ba1754623d7864250b33751bd850dd548e3ed6bd1911093"}, - {file = "ckzg-1.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:155eacc237cb28c9eafda1c47a89e6e4550f1c2e711f2eee21e0bb2f4df75546"}, - {file = "ckzg-1.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31d7fbe396a51f43375e38c31bc3a96c7996882582f95f3fcfd54acfa7b3ce6"}, - {file = "ckzg-1.0.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d3d049186c9966e9140de39a9979d7adcfe22f8b02d2852c94d3c363235cc18"}, - {file = "ckzg-1.0.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88728fbd410d61bd5d655ac50b842714c38bc34ff717f73592132d28911fc88e"}, - {file = "ckzg-1.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:052d302058d72431acc9dd4a9c76854c8dfce10c698deef5252884e32a1ac7bf"}, - {file = "ckzg-1.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:633110a9431231664be2ad32baf10971547f18289d33967654581b9ae9c94a7e"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f439c9e5297ae29a700f6d55de1525e2e295dbbb7366f0974c8702fca9e536b9"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:94f7eb080c00c0ccbd4fafad69f0b35b624a6a229a28e11d365b60b58a072832"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f876783ec654b7b9525503c2a0a1b086e5d4f52ff65cac7e8747769b0c2e5468"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7e039800e50592580171830e788ef4a1d6bb54300d074ae9f9119e92aefc568"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a8cccf0070a29bc01493179db2e61220ee1a6cb17f8ea41c68a2f043ace87f"}, - {file = "ckzg-1.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f86cef801d7b0838e17b6ee2f2c9e747447d91ad1220a701baccdf7ef11a3c8"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2433a89af4158beddebbdd66fae95b34d40f2467bee8dc40df0333de5e616b5f"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c49d5dc0918ad912777720035f9820bdbb6c7e7d1898e12506d44ab3c938d525"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:331d49bc72430a3f85ea6ecb55a0d0d65f66a21d61af5783b465906a741366d5"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86627bc33bc63b8de869d7d5bfa9868619a4f3e4e7082103935c52f56c66b5"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab6a2ba2706b5eaa1ce6bc7c4e72970bf9587e2e0e482e5fb4df1996bccb7a40"}, - {file = "ckzg-1.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8bca5e7c38d913fabc24ad09545f78ba23cfc13e1ac8250644231729ca908549"}, - {file = "ckzg-1.0.2.tar.gz", hash = "sha256:4295acc380f8d42ebea4a4a0a68c424a322bb335a33bad05c72ead8cbb28d118"}, + {file = "ckzg-2.1.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8dc23b189b5e061b5a2fcf609b6dc1f62098dd72cf9116e4721f995eb144b0ee"}, + {file = "ckzg-2.1.8-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:71e0e6d84a268c23c29d546a7ef4720a9d2aea597efe4def75205b6ef436900c"}, + {file = "ckzg-2.1.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ccabbd5aea9cf98ae5301ec8b16119720d93f8f155a567e041e2f16b1fafc87"}, + {file = "ckzg-2.1.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1dee4ec1d8e6702e6008a4d489638027cd640b5d9e5adca48a8c16aa193c6c7"}, + {file = "ckzg-2.1.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:697f5c4016a9f091bdf7e646be0d71e625b5106a9f78c6ab53e93e5f425aa513"}, + {file = "ckzg-2.1.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:50f6281f5c55240f1be254041e0c06d6348c91e21fccd0ed108151f880b6d087"}, + {file = "ckzg-2.1.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9371ac89c420beb6f6d236ce808c7990174dc21bde61032c39235982e4e65697"}, + {file = "ckzg-2.1.8-cp310-cp310-win_amd64.whl", hash = "sha256:c1f0f8e7a14bb4348f3ecaee92d929df3428be1f706034c961e11239d398bcdb"}, + {file = "ckzg-2.1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:adb4868ec73547d040d8551538dc00ebc3822a758a7fb2b762735e097556c13b"}, + {file = "ckzg-2.1.8-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:5e42a07899e62cf8888bdefe1fa200e3248190953bacc1d872e13825b0b91a7f"}, + {file = "ckzg-2.1.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:369af9ec31248a942f2b249bee9f60dccfe6c8ac944095ad26c8083c9df04186"}, + {file = "ckzg-2.1.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac8202240347c4af5cee9a3ebc62560ae629c8bac2b1ee313fa57cf1c9e4f0a"}, + {file = "ckzg-2.1.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eff52513b32bc83e3b978cae991289cc932d801b6c08aa002d29e44a4290b2f0"}, + {file = "ckzg-2.1.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:23bef3d98c905152f4ff497a6243b6a14e916deef2d8a1d342175be90f6e6425"}, + {file = "ckzg-2.1.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e6e33672e474331519b7d07b8c8e0cbec3d817b6341e00fa728a94141fa8e8"}, + {file = "ckzg-2.1.8-cp311-cp311-win_amd64.whl", hash = "sha256:b84d698c81569381a3dd18a9848f8fca03273ae62045494cee5669a885697d8d"}, + {file = "ckzg-2.1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:98abe138d79886e3e1fbbaf05cdf0702a4351f242ad1a8b4802343c7ba149faa"}, + {file = "ckzg-2.1.8-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:2fe01dad7c968bcdf3c063c5192bc7d7d59f66358afb5c99554e5ce2435a95aa"}, + {file = "ckzg-2.1.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bdb7f51ee3cf8e45451bee8c6dce975fddadfe231174d8de9c27a3aa27741b8"}, + {file = "ckzg-2.1.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10339d23e36b8a0a4e6fda7f6c72d6b2fd4e1506f7b64a661ba8c706ee33f335"}, + {file = "ckzg-2.1.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4456c9027f2edcb50a2279d6035ca971a511d8b0025e6659ff407b87ad841ba"}, + {file = "ckzg-2.1.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88cbec7e2010f64988c249ebb5679b73ccae1c536f4c130d4708bf7b06a8cd69"}, + {file = "ckzg-2.1.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4d2581df10bdfaec00fec6daefdeaa438e66582364e1d1b705710e9d749fc47"}, + {file = "ckzg-2.1.8-cp312-cp312-win_amd64.whl", hash = "sha256:a30f2b980929e898f0b28aa6bf9ae35e7afd5884e354376ad3744669b7cacf3e"}, + {file = "ckzg-2.1.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:26ed4c4d3acbfdb4bfdf1ab1029219657d4565e1c63d36f2695cdfdb5ec0b569"}, + {file = "ckzg-2.1.8-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:a6899908ca3a41e6d2aa19973b398de101a6d64b5189894077ea09a3f508d3fd"}, + {file = "ckzg-2.1.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3db1ca21685d567eea668925c9f85ebd723db41d24bbecaa2f78d8256e1ba9c6"}, + {file = "ckzg-2.1.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f27d7d16be9debf173369248ff06e97fc45826bfb0a743519b49b38539ab6c7"}, + {file = "ckzg-2.1.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dadf0cd0c3c19611e8a1188a2a10316b88fbae56d806f75a67cbe846b8b7ec86"}, + {file = "ckzg-2.1.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cde13789188b7bac6d5bee308532a7bd60ab5c2feeccdef2f1da41be761c7e04"}, + {file = "ckzg-2.1.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f2737534458547ba8ce89663f833041925b104f7906f17b2338d1b36e6c7c4d"}, + {file = "ckzg-2.1.8-cp313-cp313-win_amd64.whl", hash = "sha256:10b483ad6937878f03d556d120a43d323dcb3891eb83313aa71087b54559594b"}, + {file = "ckzg-2.1.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f3e4a3ae1ff3ec811b6d2aac7a246524f72a750af95fe7a01550cdd68677d6f"}, + {file = "ckzg-2.1.8-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:9afe50a28d8d6f130797f0861d4753f76294d983f1e6ead9c17dfa14f8118ad4"}, + {file = "ckzg-2.1.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da197eef7014997b976ae7bc6e0ad42c02a9faa3a4221d6699e8b777761422f1"}, + {file = "ckzg-2.1.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fef164b9a0c7ed57935bf78bd23e701ba82efcca180e9b29814a94928a2d5880"}, + {file = "ckzg-2.1.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c56f588a57d419ac314880931122796e4331be395368c8887ca3ade5c27f539c"}, + {file = "ckzg-2.1.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:37eecbce59271040dfab736db490a28c0e59f189b404e5820e65531dadf84ddb"}, + {file = "ckzg-2.1.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:acc5d33e42ac852fec08ad1991020a958e2534ac6af346a83f579b553bd0bdfd"}, + {file = "ckzg-2.1.8-cp314-cp314-win_amd64.whl", hash = "sha256:5eb3b5327dd0cbaaa6551e01a42af9780e998640c57236e460830bf9a6e6f9b4"}, + {file = "ckzg-2.1.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dbd7021cdc5616df5902ec04876b9af25d17facc39851b4d6630c9c2b209e30a"}, + {file = "ckzg-2.1.8-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:5ecbcd887fa97988ddfc3ac4d1951367fa4f6bb25a6f72d449550ea4be45b938"}, + {file = "ckzg-2.1.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaeaed98d7be94f7c215373f90cd06536237d6ec08d48d2be74c630e03edf73c"}, + {file = "ckzg-2.1.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1030903bf9989957b73fe30a96516e75a8bc27efb65e1b6e9d3507447bfee06"}, + {file = "ckzg-2.1.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff48587f541b625dd0e471ecc866cf91462100a4429e3c21cf4f099e7dff9150"}, + {file = "ckzg-2.1.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0580e8a1780d44a85c6edaa44e30a6a85565ad26593becb320d7353bc05e8627"}, + {file = "ckzg-2.1.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bc8a259bc3cc321413c3c8eb0789cbe35919f4f4bb32946b0e1a484d4303629c"}, + {file = "ckzg-2.1.8-cp314-cp314t-win_amd64.whl", hash = "sha256:f41377a2a63330df64ae6f7cd806a288b21c52aa346151fd8f0551b9e0742289"}, + {file = "ckzg-2.1.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c4ec8234a72cebe419330e13d580048fec25e63d9526a0a9bf65ed134b6ffa9a"}, + {file = "ckzg-2.1.8-cp39-cp39-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:036b8de43a07340609a3a4445d4da542cb9487c58dc0752c2d419c7be1f3dbdb"}, + {file = "ckzg-2.1.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9290e8d128921f1906ffa8e6229a878a9e967ea9937697bf90c894fe523e9d11"}, + {file = "ckzg-2.1.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad90fe4e55aea73e3df744b97043c43cd3227725c3f8715fbae07b4eb0acbab0"}, + {file = "ckzg-2.1.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8d511e410956aa2fcde7d19f40cefcc166fa84ef3387b3c96083e3798bd21a44"}, + {file = "ckzg-2.1.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e523457f3a463214e2ae8aa37f8b35d519e1a51d772734d381dd090cf7391b5c"}, + {file = "ckzg-2.1.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:70b67a92373b834e910bc527d538c170757ebc70bc8f99beedbfd64d1913dc80"}, + {file = "ckzg-2.1.8-cp39-cp39-win_amd64.whl", hash = "sha256:d2e709e9701d6085d79cf5d2ed12d373beffae1d93d61b0387055a14818973c0"}, + {file = "ckzg-2.1.8.tar.gz", hash = "sha256:d7bef6b425dca6995457fc59fc5b30211d9b28cbbeee0e7a7bef1372e13f29ca"}, ] [[package]] @@ -625,7 +549,6 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" @@ -701,126 +624,198 @@ files = [ {file = "coverage-7.2.2.tar.gz", hash = "sha256:36dd42da34fe94ed98c39887b86db9d06777b1c8f860520e21126a75507024f2"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cytoolz" -version = "0.12.1" +version = "1.1.0" description = "Cython implementation of Toolz: High performance functional utilities" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" groups = ["main"] -markers = "implementation_name == \"cpython\"" -files = [ - {file = "cytoolz-0.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c59bb4ca88e1c69931468bf21f91c8f64d8bf1999eb163b7a2df336f60c304a"}, - {file = "cytoolz-0.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d700e011156ff112966c6d77faaae125fcaf538f4cec2b9ce8957de82858f0f"}, - {file = "cytoolz-0.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23c3f57c48eb939d2986eba4aeaeedf930ebf94d58c91a42d4e0fc45ed5427dc"}, - {file = "cytoolz-0.12.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25ff13c468c06da9ef26651dc389e7e8bb7af548f8c1dfb96305f57f18d398a8"}, - {file = "cytoolz-0.12.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a734511144309ea6e105406633affb74e303a3df07d8a3954f9b01946e27ecb1"}, - {file = "cytoolz-0.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48bc2f30d1b2646d675bb8e7778ab59379bf9edc59fe06fb0e7f85ba1271bf44"}, - {file = "cytoolz-0.12.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30936ae8fa68b6a1ac8ad6c4bacb5a8a00d51bc6c89f9614a1557b0105d09f8a"}, - {file = "cytoolz-0.12.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efd1b2da3ee577fcfa723a214f73186aef9674dd5b28242d90443c7a82722b0f"}, - {file = "cytoolz-0.12.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6805b007af3557ee6c20dab491b6e55a8177f5b6845d9e6c653374d540366ba7"}, - {file = "cytoolz-0.12.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a6e63fc67b23830947b51e0a488992e3c904fce825ead565f3904dcf621d05f7"}, - {file = "cytoolz-0.12.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9e324a94856d88ecf10f34c102d0ded67d7c3cf644153d77e34a29720ce6aa47"}, - {file = "cytoolz-0.12.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:02975e2b1e61e47e9afa311f4c1783d155136fad37c54a1cebfe991c5a0798a1"}, - {file = "cytoolz-0.12.1-cp310-cp310-win32.whl", hash = "sha256:b6569f6038133909cd658dbdcc6fc955f791dc47a7f5b55d2066f742253dcbfe"}, - {file = "cytoolz-0.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:1be368623e46ad3c1ce807e7a436acb119c26001507b31f92ceb21b86e08c386"}, - {file = "cytoolz-0.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:849f461bffa1e7700ccfcb5186df29cd4cdcc9efdb7199cb8b5681dc37045d72"}, - {file = "cytoolz-0.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4284120c978fb7039901bf6e66832cb3e82ac1b2a107512e735bdb04fd5533ed"}, - {file = "cytoolz-0.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ec296f01c29c809698eaf677211b6255691295c2b35caab2131e1e7eaadfbac"}, - {file = "cytoolz-0.12.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37c53f456a1c84566a7d911eec57c4c6280b915ab0600e7671582793cc2769fe"}, - {file = "cytoolz-0.12.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b6761791973b1e839b8309d5853b40eeb413368e31beaf5f2b6ed44c6fc7cf0"}, - {file = "cytoolz-0.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff478682e8ee6dbaa37201bb71bf4a6eee744006ab000e8f5cea05066fc7c845"}, - {file = "cytoolz-0.12.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:867bebe6be30ee36a836f9b835790762a74f46be8cc339ea57b68dcecdbc1133"}, - {file = "cytoolz-0.12.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7e903df991f0957e2b271a37bb25d28e0d260c52825ae67507d15ca55a935961"}, - {file = "cytoolz-0.12.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e797c4afb1b7962d3205b1959e1051f7e6bfbba29da44042a9efc2391f1feb38"}, - {file = "cytoolz-0.12.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b8eceaa12b7f152b046b67cb053ec2b5b00f73593983de69bc5e63a8aca4a7a8"}, - {file = "cytoolz-0.12.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b575393dd431b8e211de35bd593d831dac870172b16e2b7934f3566b8fc89377"}, - {file = "cytoolz-0.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3032c0ba42dee5836d6b57a72a569b65df2c29e8ed266cb900d569003cf933a9"}, - {file = "cytoolz-0.12.1-cp311-cp311-win32.whl", hash = "sha256:c576bd63495150385b8d05eaae775387f378be2fd9805d3ffb4d17c87271fbad"}, - {file = "cytoolz-0.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:421b224dc4157a0d66625acb5798cf50858cfa06a5232d39a8bd6cf1fa88aca3"}, - {file = "cytoolz-0.12.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:be5a454a95797343d0fb1ed02caecae73a023b1393c112951c84f17ec9f4076c"}, - {file = "cytoolz-0.12.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:061387aa39b9c1576c25d0c59142513c09e77a2a07bd5d6211a43c7a758b6f45"}, - {file = "cytoolz-0.12.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14f4dbc3f0ec8f6fc68865489af21dcf042ff007d2737c27bfd73296f15db544"}, - {file = "cytoolz-0.12.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a816bff6bf424753e1ac2441902ceaf37ae6718b745a53f6aa1a60c617fb4f5f"}, - {file = "cytoolz-0.12.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633f19d1990b1cf9c67dce9c28bf8b5a18e42785d15548607a100e1236384d5d"}, - {file = "cytoolz-0.12.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fa7009c843667868aa8bdb3d68e5ef3d6356dd418b17ed5ca4e1340e82483a5"}, - {file = "cytoolz-0.12.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:1c29dd04e282ddfd45b457e3551075beec9128aa9271245e58ce924bf6e055f8"}, - {file = "cytoolz-0.12.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd35c0be4c46274129dd1678bb911dd4e93d23968b26f4e39cd55bc7cb3b1bac"}, - {file = "cytoolz-0.12.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5158ae6d8dd112d003f677039a3613ca7d2592bfe35d7accf23684edb961fc26"}, - {file = "cytoolz-0.12.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:7eb9e6fa8a82c3d2f519f7d3942898a97792e3895569e9501b9431048289b82f"}, - {file = "cytoolz-0.12.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ac6784cc43aec51a86cf9058a2a343084f8cf46a9281bea5762bfa608127c53b"}, - {file = "cytoolz-0.12.1-cp36-cp36m-win32.whl", hash = "sha256:794cce219bbcb2f36ca220f27d5afd64eaa854e04901bd6f240be156a578b607"}, - {file = "cytoolz-0.12.1-cp36-cp36m-win_amd64.whl", hash = "sha256:695dd8231e4f1bfb9a2363775a6e4e56ad9d2058058f817203a49614f4bfe33b"}, - {file = "cytoolz-0.12.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1bd8017ef0da935a20106272c5f5ff6b1114add1ccb09cfed1ff7ec5cc01c6d"}, - {file = "cytoolz-0.12.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56e1ebf6eb4438b8c45cbe7e7b22fc65df0c9efa97a70d3bf2f51e08b19756a5"}, - {file = "cytoolz-0.12.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:816c2038008ebf50d81171ddfae377f1af9e71d504ec609469dcb0906bfcf2ae"}, - {file = "cytoolz-0.12.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bebe58f7a160db7838eb70990c704db4bdc2d58bd364290fd69be0587be8bac"}, - {file = "cytoolz-0.12.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a72440305f634604827f96810e4469877b89f5c060d6852267650a49b0e3768c"}, - {file = "cytoolz-0.12.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b46ebc463bb45f278a2b94e630061c26e10077cb68d4c93583d8f4199699a5ef"}, - {file = "cytoolz-0.12.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e75e287787e6adafed9d8c3d3e7647c0b5eb460221f9f92d7dfe48b45ba77c0d"}, - {file = "cytoolz-0.12.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:03ab22c9aeb1535f8647d23b6520b0c3d41aaa18d04ef42b352dde1931f2e2b1"}, - {file = "cytoolz-0.12.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b2ac288f27a2689d9e39f4cf4df5437a8eb038eaae515169586c77f9f8fb343a"}, - {file = "cytoolz-0.12.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:97a24c0d0806fcf9a6e75fc18aeb95adc37eb0baf6451f10a2de23ffd815329d"}, - {file = "cytoolz-0.12.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:42c9e5cd2a48a257b1f2402334b48122501f249b8dcf77082f569f2680f185eb"}, - {file = "cytoolz-0.12.1-cp37-cp37m-win32.whl", hash = "sha256:35fae4eaa0eaf9072a5fe2d244a79e65baae4e5ddbe9cc629c5037af800213a2"}, - {file = "cytoolz-0.12.1-cp37-cp37m-win_amd64.whl", hash = "sha256:5af43ca7026ead3dd08b261e4f7163cd2cf3ceaa74fa5a81f7b7ea5d445e41d6"}, - {file = "cytoolz-0.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fcc378fa97f02fbcef090b3611305425d72bd1c0afdd13ef4a82dc67d40638b6"}, - {file = "cytoolz-0.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc3645cf6b9246cb8e179db2803e4f0d148211d2a2cf22d5c9b5219111cd91a0"}, - {file = "cytoolz-0.12.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b245b824f4705aef0e4a03fafef3ad6cb59ef43cc564cdbf683ee28dfc11ad5"}, - {file = "cytoolz-0.12.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1964dcb5f250fd13fac210944b20810d61ef4094a17fbbe502ab7a7eaeeace7"}, - {file = "cytoolz-0.12.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7194a22a4a24f3561cb6ad1cca9c9b2f2cf34cc8d4bce6d6a24c80960323fa8"}, - {file = "cytoolz-0.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c5434db53f3a94a37ad8aedb231901e001995d899af6ed1165f3d27fa04a6a"}, - {file = "cytoolz-0.12.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b30cd083ef8af4ba66d9fe5cc75c653ede3f2655f97a032db1a14cc8a006719c"}, - {file = "cytoolz-0.12.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bef934bd3e024d512c6c0ad1c66eb173f61d9ccb4dbca8d75f727a5604f7c2f6"}, - {file = "cytoolz-0.12.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37320669c364f7d370392af33cc1034b4563da66c22cd3261e3530f4d30dbe4b"}, - {file = "cytoolz-0.12.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3cb95d23defb2322cddf70efb4af6dac191d95edaa343e8c1f58f1afa4f92ecd"}, - {file = "cytoolz-0.12.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ac5895d5f78dbd8646fe37266655ba4995f9cfec38a86595282fee69e41787da"}, - {file = "cytoolz-0.12.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:499af2aff04f65b4c23de1df08e1d1484a93b23ddaaa0163e44b5070b68356eb"}, - {file = "cytoolz-0.12.1-cp38-cp38-win32.whl", hash = "sha256:aa61e3da751a2dfe95aeca603f3ef510071a136ba9905f61ae6cb5d0696271ad"}, - {file = "cytoolz-0.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:f5b43ce952a5a31441556c55f5f5f5a8e62c28581a0ff2a2c31c04ef992d73bd"}, - {file = "cytoolz-0.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b8f88251b84b3877254cdd59c86a1dc6b2b39a03c6c9c067d344ef879562e0"}, - {file = "cytoolz-0.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d72415b0110f7958dd3a5ee98a70166f47bd42ede85e3535669c794d06f57406"}, - {file = "cytoolz-0.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8101ab6de5aa0b26a2b5032bc488d430010c91863e701812d65836b03a12f61"}, - {file = "cytoolz-0.12.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2eed428b5e68c28abf2c71195e799850e040d67a27c05f7785319c611665b86a"}, - {file = "cytoolz-0.12.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59641eb1f41cb688b3cb2f98c9003c493a5024325f76b5c02333d08dd972127c"}, - {file = "cytoolz-0.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a48940ff0449ffcf690310bf9228bb57885f7571406ed2fe05c98e299987195"}, - {file = "cytoolz-0.12.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bae431a5985cdb2014be09d37206c288e0d063940cf9539e9769bd2ec26b220"}, - {file = "cytoolz-0.12.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cb8b10405960a8e6801a4702af98ea640130ec6ecfc1208195762de3f5503ba9"}, - {file = "cytoolz-0.12.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c9a16a5b4f54d5c0a131f56b0ca65998a9a74958b5b36840c280edba4f8b907"}, - {file = "cytoolz-0.12.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:49911cb533c96d275e31e7eaeb0742ac3f7afe386a1d8c40937814d75039a0f7"}, - {file = "cytoolz-0.12.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:dbae37d48ef5a0ab90cfaf2b9312d96f034b1c828208a9cbe25377a1b19ba129"}, - {file = "cytoolz-0.12.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c34e69be4429633fc614febe3127fa03aa418a1abb9252f29d9ba5b3394573a5"}, - {file = "cytoolz-0.12.1-cp39-cp39-win32.whl", hash = "sha256:0d474dacbafbdbb44c7de986bbf71ff56ae62df0d52ab3b6fa966784dc88737a"}, - {file = "cytoolz-0.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:3d6d0b0075731832343eb88229cea4bf39e96f3fc7acbc449aadbdfec2842703"}, - {file = "cytoolz-0.12.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8506d1863f30d26f577c4ed59d2cfd03d2f39569f9cbaa02a764a9de73d312d5"}, - {file = "cytoolz-0.12.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a1eae39656a1685e8b3f433eecfd72015ce5c1d7519e9c8f9402153c68331bb"}, - {file = "cytoolz-0.12.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a0055943074c6c85b77fcc3f42f7c54010a3478daa2ed9d6243d0411c84a4d3"}, - {file = "cytoolz-0.12.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a7a325b8fe885a6dd91093616c703134f2dacbd869bc519970df3849c2a15b"}, - {file = "cytoolz-0.12.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:7b60caf0fa5f1b49f1062f7dc0f66c7b23e2736bad50fa8296bfb845995e3051"}, - {file = "cytoolz-0.12.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:980e7eb7205e01816a92f3290cfc80507957e64656b9271a0dfebb85fe3718c0"}, - {file = "cytoolz-0.12.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d38a40fe153f23cda0e823413fe9d9ebee89dd461827285316eff929fb121e"}, - {file = "cytoolz-0.12.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d540e9c34a61b53b6a374ea108794a48388178f7889d772e364cdbd6df37774c"}, - {file = "cytoolz-0.12.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:117871f036926e42d3abcee587eafa9dc7383f1064ac53a806d33e76604de311"}, - {file = "cytoolz-0.12.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:31131b54a0c72efc0eb432dc66df546c6a54f2a7d396c9a34cf65ac1c26b1df8"}, - {file = "cytoolz-0.12.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4534cbfad73cdb1a6dad495530d4186d57d73089c01e9cb0558caab50e46cb3b"}, - {file = "cytoolz-0.12.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50db41e875e36aec11881b8b12bc69c6f4836b7dd9e88a9e5bbf26c2cb3ba6cd"}, - {file = "cytoolz-0.12.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6716855f9c669c9e25a185d88e0f169839bf8553d16496796325acd114607c11"}, - {file = "cytoolz-0.12.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f32452e833f0605b871626e6c61b71b0cba24233aad0e04accc3240497d4995"}, - {file = "cytoolz-0.12.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ba74c239fc6cb6e962eabc420967c7565f3f363b776c89b3df5234caecf1f463"}, - {file = "cytoolz-0.12.1.tar.gz", hash = "sha256:fc33909397481c90de3cec831bfb88d97e220dc91939d996920202f184b4648e"}, +files = [ + {file = "cytoolz-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:72d7043a88ea5e61ba9d17ea0d1c1eff10f645d7edfcc4e56a31ef78be287644"}, + {file = "cytoolz-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d759e9ed421bacfeb456d47af8d734c057b9912b5f2441f95b27ca35e5efab07"}, + {file = "cytoolz-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fdb5be8fbcc0396141189022724155a4c1c93712ac4aef8c03829af0c2a816d7"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c8c0a513dc89bc05cc72893609118815bced5ef201f1a317b4cc3423b3a0e750"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce94db4f8ebe842c30c0ece42ff5de977c47859088c2c363dede5a68f6906484"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b622d4f54e370c853ded94a668f94fe72c6d70e06ac102f17a2746661c27ab52"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:375a65baa5a5b4ff6a0c5ff17e170cf23312e4c710755771ca966144c24216b5"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c0d51bcdb3203a062a78f66bbe33db5e3123048e24a5f0e1402422d79df8ee2d"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1010869529bb05dc9802b6d776a34ca1b6d48b9deec70ad5e2918ae175be5c2f"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a8f2e83295bdb33f35454d6bafcb7845b03b5881dcaed66ecbd726c7f16772"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0499c5e0a8e688ed367a2e51cc13792ae8f08226c15f7d168589fc44b9b9cada"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87d44e6033d4c5e95a7d39ba59b8e105ba1c29b1ccd1d215f26477cc1d64be39"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a68cef396a7de237f7b97422a6a450dfb111722296ba217ba5b34551832f1f6e"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:06ad4c95b258141f138a93ebfdc1d76ac087afc1a82f1401100a1f44b44ba656"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ada59a4b3c59d4ac7162e0ed08667ffa78abf48e975c8a9f9d5b9bc50720f4fd"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a8957bcaea1ba01327a9b219d2adb84144377684f51444253890dab500ca171f"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6d8cdc299d67eb0f3b9ecdafeeb55eb3b7b7470e2d950ac34b05ed4c7a5572b8"}, + {file = "cytoolz-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d8e08464c5cdea4f6df31e84b11ed6bfd79cedb99fbcbfdc15eb9361a6053c5a"}, + {file = "cytoolz-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e49922a7ed54262d41960bf3b835a7700327bf79cff1e9bfc73d79021132ff8"}, + {file = "cytoolz-1.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:943a662d2e72ffc4438d43ab5a1de8d852237775a423236594a3b3e381b8032c"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dba8e5a8c6e3c789d27b0eb5e7ce5ed7d032a7a9aae17ca4ba5147b871f6e327"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44b31c05addb0889167a720123b3b497b28dd86f8a0aeaf3ae4ffa11e2c85d55"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:653cb18c4fc5d8a8cfce2bce650aabcbe82957cd0536827367d10810566d5294"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:853a5b4806915020c890e1ce70cc056bbc1dd8bc44f2d74d555cccfd7aefba7d"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7b44e9de86bea013fe84fd8c399d6016bbb96c37c5290769e5c99460b9c53e5"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:098d628a801dc142e9740126be5624eb7aef1d732bc7a5719f60a2095547b485"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:779ee4096ed7a82cffab89372ffc339631c285079dbf33dbe7aff1f6174985df"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f2ce18dd99533d077e9712f9faa852f389f560351b1efd2f2bdb193a95eddde2"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac266a34437812cf841cecbfe19f355ab9c3dd1ef231afc60415d40ff12a76e4"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1920b9b9c13d60d0bb6cd14594b3bce0870022eccb430618c37156da5f2b7a55"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47caa376dafd2bdc29f8a250acf59c810ec9105cd6f7680b9a9d070aae8490ec"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ab2c97d8aaa522b038cca9187b1153347af22309e7c998b14750c6fdec7b1cb"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4bce006121b120e8b359244ee140bb0b1093908efc8b739db8dbaa3f8fb42139"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fc0f1e4e9bb384d26e73c6657bbc26abdae4ff66a95933c00f3d578be89181b"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd3f894ff972da1994d06ac6157d74e40dda19eb31fe5e9b7863ca4278c3a167"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0846f49cf8a4496bd42659040e68bd0484ce6af819709cae234938e039203ba0"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:16a3af394ade1973226d64bb2f9eb3336adbdea03ed5b134c1bbec5a3b20028e"}, + {file = "cytoolz-1.1.0-cp311-cp311-win32.whl", hash = "sha256:b786c9c8aeab76cc2f76011e986f7321a23a56d985b77d14f155d5e5514ea781"}, + {file = "cytoolz-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ebf06d1c5344fb22fee71bf664234733e55db72d74988f2ecb7294b05e4db30c"}, + {file = "cytoolz-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b63f5f025fac893393b186e132e3e242de8ee7265d0cd3f5bdd4dda93f6616c9"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:99f8e134c9be11649342853ec8c90837af4089fc8ff1e8f9a024a57d1fa08514"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6f44cf9319c30feb9a50aa513d777ef51efec16f31c404409e7deb8063df64"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:945580dc158c557172fca899a35a99a16fbcebf6db0c77cb6621084bc82189f9"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:257905ec050d04f2f856854620d1e25556fd735064cebd81b460f54939b9f9d5"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82779049f352fb3ab5e8c993ab45edbb6e02efb1f17f0b50f4972c706cc51d76"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7d3e405e435320e08c5a1633afaf285a392e2d9cef35c925d91e2a31dfd7a688"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:923df8f5591e0d20543060c29909c149ab1963a7267037b39eee03a83dbc50a8"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:25db9e4862f22ea0ae2e56c8bec9fc9fd756b655ae13e8c7b5625d7ed1c582d4"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7a98deb11ccd8e5d9f9441ef2ff3352aab52226a2b7d04756caaa53cd612363"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dce4ee9fc99104bc77efdea80f32ca5a650cd653bcc8a1d984a931153d3d9b58"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80d6da158f7d20c15819701bbda1c041f0944ede2f564f5c739b1bc80a9ffb8b"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3b5c5a192abda123ad45ef716ec9082b4cf7d95e9ada8291c5c2cc5558be858b"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5b399ce7d967b1cb6280250818b786be652aa8ddffd3c0bb5c48c6220d945ab5"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e7e29a1a03f00b4322196cfe8e2c38da9a6c8d573566052c586df83aacc5663c"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5291b117d71652a817ec164e7011f18e6a51f8a352cc9a70ed5b976c51102fda"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8caef62f846a9011676c51bda9189ae394cdd6bb17f2946ecaedc23243268320"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:de425c5a8e3be7bb3a195e19191d28d9eb3c2038046064a92edc4505033ec9cb"}, + {file = "cytoolz-1.1.0-cp312-cp312-win32.whl", hash = "sha256:296440a870e8d1f2e1d1edf98f60f1532b9d3ab8dfbd4b25ec08cd76311e79e5"}, + {file = "cytoolz-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:07156987f224c6dac59aa18fb8bf91e1412f5463961862716a3381bf429c8699"}, + {file = "cytoolz-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:23e616b38f5b3160c7bb45b0f84a8f3deb4bd26b29fb2dfc716f241c738e27b8"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:76c9b58555300be6dde87a41faf1f97966d79b9a678b7a526fcff75d28ef4945"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d1d638b10d3144795655e9395566ce35807df09219fd7cacd9e6acbdef67946a"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:26801c1a165e84786a99e03c9c9973356caaca002d66727b761fb1042878ef06"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a9a464542912d3272f6dccc5142df057c71c6a5cbd30439389a732df401afb7"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed6104fa942aa5784bf54f339563de637557e3443b105760bc4de8f16a7fc79b"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56161f0ab60dc4159ec343509abaf809dc88e85c7e420e354442c62e3e7cbb77"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:832bd36cc9123535f1945acf6921f8a2a15acc19cfe4065b1c9b985a28671886"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1842636b6e034f229bf084c2bcdcfd36c8437e752eefd2c74ce9e2f10415cb6e"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:823df012ab90d2f2a0f92fea453528539bf71ac1879e518524cd0c86aa6df7b9"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f1fcf9e7e7b3487883ff3f815abc35b89dcc45c4cf81c72b7ee457aa72d197b"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4cdb3fa1772116827f263f25b0cdd44c663b6701346a56411960534a06c082de"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1b5c95041741b81430454db65183e133976f45ac3c03454cfa8147952568529"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2079fd9f1a65f4c61e6278c8a6d4f85edf30c606df8d5b32f1add88cbbe2286"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a92a320d72bef1c7e2d4c6d875125cf57fc38be45feb3fac1bfa64ea401f54a4"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06d1c79aa51e6a92a90b0e456ebce2288f03dd6a76c7f582bfaa3eda7692e8a5"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e1d7be25f6971e986a52b6d3a0da28e1941850985417c35528f6823aef2cfec5"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:964b248edc31efc50a65e9eaa0c845718503823439d2fa5f8d2c7e974c2b5409"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c9ff2b3c57c79b65cb5be14a18c6fd4a06d5036fb3f33e973a9f70e9ac13ca28"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:22290b73086af600042d99f5ce52a43d4ad9872c382610413176e19fc1d4fd2d"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ade74fccd080ea793382968913ee38d7a35c921df435bbf0a6aeecf0d17574"}, + {file = "cytoolz-1.1.0-cp313-cp313-win32.whl", hash = "sha256:db5dbcfda1c00e937426cbf9bdc63c24ebbc358c3263bfcbc1ab4a88dc52aa8e"}, + {file = "cytoolz-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e2d3fe3b45c3eb7233746f7aca37789be3dceec3e07dcc406d3e045ea0f7bdc"}, + {file = "cytoolz-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:32c559f95ff44a9ebcbd934acaa1e6dc8f3e6ffce4762a79a88528064873d6d5"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9e2cd93b28f667c5870a070ab2b8bb4397470a85c4b204f2454b0ad001cd1ca3"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f494124e141a9361f31d79875fe7ea459a3be2b9dadd90480427c0c52a0943d4"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53a3262bf221f19437ed544bf8c0e1980c81ac8e2a53d87a9bc075dba943d36f"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:47663e57d3f3f124921f38055e86a1022d0844c444ede2e8f090d3bbf80deb65"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5a8755c4104ee4e3d5ba434c543b5f85fdee6a1f1df33d93f518294da793a60"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d96ff3d381423af1b105295f97de86d1db51732c9566eb37378bab6670c5010"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0ec96b3d537cdf47d4e76ded199f7440715f4c71029b45445cff92c1248808c2"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:208e2f2ef90a32b0acbff3303d90d89b13570a228d491d2e622a7883a3c68148"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d416a81bb0bd517558668e49d30a7475b5445f9bbafaab7dcf066f1e9adba36"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f32e94c91ffe49af04835ee713ebd8e005c85ebe83e7e1fdcc00f27164c2d636"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15d0c6405efc040499c46df44056a5c382f551a7624a41cf3e4c84a96b988a15"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:bf069c5381d757debae891401b88b3a346ba3a28ca45ba9251103b282463fad8"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d5cf15892e63411ec1bd67deff0e84317d974e6ab2cdfefdd4a7cea2989df66"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3e3872c21170f8341656f8692f8939e8800dcee6549ad2474d4c817bdefd62cd"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b9ddeff8e8fd65eb1fcefa61018100b2b627e759ea6ad275d2e2a93ffac147bf"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:02feeeda93e1fa3b33414eb57c2b0aefd1db8f558dd33fdfcce664a0f86056e4"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d08154ad45349162b6c37f12d5d1b2e6eef338e657b85e1621e4e6a4a69d64cb"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win32.whl", hash = "sha256:10ae4718a056948d73ca3e1bb9ab1f95f897ec1e362f829b9d37cc29ab566c60"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1bb77bc6197e5cb19784b6a42bb0f8427e81737a630d9d7dda62ed31733f9e6c"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:563dda652c6ff52d215704fbe6b491879b78d7bbbb3a9524ec8e763483cb459f"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d542cee7c7882d2a914a33dec4d3600416fb336734df979473249d4c53d207a1"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31922849b701b0f24bb62e56eb2488dcd3aa6ae3057694bd6b3b7c4c2bc27c2f"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e68308d32afd31943314735c1335e4ab5696110e96b405f6bdb8f2a8dc771a16"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc4bb48b3b866e1867f7c6411a4229e5b44be3989060663713e10efc24c9bd5f"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:456f77207d1445025d7ef262b8370a05492dcb1490cb428b0f3bf1bd744a89b0"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:174ebc71ebb20a9baeffce6ee07ee2cd913754325c93f99d767380d8317930f7"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8b3604fef602bcd53415055a4f68468339192fd17be39e687ae24f476d23d56e"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3604b959a01f64c366e7d10ec7634d5f5cfe10301e27a8f090f6eb3b2a628a18"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6db2127a3c1bc2f59f08010d2ae53a760771a9de2f67423ad8d400e9ba4276e8"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56584745ac647993a016a21bc76399113b7595e312f8d0a1b140c9fcf9b58a27"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db2c4c3a7f7bd7e03bb1a236a125c8feb86c75802f4ecda6ecfaf946610b2930"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48cb8a692111a285d2b9acd16d185428176bfbffa8a7c274308525fccd01dd42"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d2f344ba5eb17dcf38ee37fdde726f69053f54927db8f8a1bed6ac61e5b1890d"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abf76b1c1abd031f098f293b6d90ee08bdaa45f8b5678430e331d991b82684b1"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ddf9a38a5b686091265ff45b53d142e44a538cd6c2e70610d3bc6be094219032"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:946786755274f07bb2be0400f28adb31d7d85a7c7001873c0a8e24a503428fb3"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b8f78b9fed79cf185ad4ddec099abeef45951bdcb416c5835ba05f0a1242c7"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fccde6efefdbc02e676ccb352a2ccc8a8e929f59a1c6d3d60bb78e923a49ca44"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:717b7775313da5f51b0fbf50d865aa9c39cb241bd4cb605df3cf2246d6567397"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5158744a09d0e0e4a4f82225e3a3c4ebf38f9ae74467aaa905467270e52f2794"}, + {file = "cytoolz-1.1.0-cp314-cp314-win32.whl", hash = "sha256:1ed534bdbbf063b2bb28fca7d0f6723a3e5a72b086e7c7fe6d74ae8c3e4d00e2"}, + {file = "cytoolz-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:472c1c9a085f5ad973ec0ad7f0b9ba0969faea6f96c9e397f6293d386f3a25ec"}, + {file = "cytoolz-1.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:a7ad7ca3386fa86bd301be3fa36e7f0acb024f412f665937955acfc8eb42deff"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:64b63ed4b71b1ba813300ad0f06b8aff19a12cf51116e0e4f1ed837cea4debcf"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a60ba6f2ed9eb0003a737e1ee1e9fa2258e749da6477946008d4324efa25149f"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1aa58e2434d732241f7f051e6f17657e969a89971025e24578b5cbc6f1346485"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6965af3fc7214645970e312deb9bd35a213a1eaabcfef4f39115e60bf2f76867"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddd2863f321d67527d3b67a93000a378ad6f967056f68c06467fe011278a6d0e"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4e6b428e9eb5126053c2ae0efa62512ff4b38ed3951f4d0888ca7005d63e56f5"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d758e5ef311d2671e0ae8c214c52e44617cf1e58bef8f022b547b9802a5a7f30"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a95416eca473e6c1179b48d86adcf528b59c63ce78f4cb9934f2e413afa9b56b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36c8ede93525cf11e2cc787b7156e5cecd7340193ef800b816a16f1404a8dc6d"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c949755b6d8a649c5fbc888bc30915926f1b09fe42fea9f289e297c2f6ddd3"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1b6d37545816905a76d9ed59fa4e332f929e879f062a39ea0f6f620405cdc27"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05332112d4087904842b36954cd1d3fc0e463a2f4a7ef9477bd241427c593c3b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:31538ca2fad2d688cbd962ccc3f1da847329e2258a52940f10a2ac0719e526be"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:747562aa70abf219ea16f07d50ac0157db856d447f7f498f592e097cbc77df0b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3dc15c48b20c0f467e15e341e102896c8422dccf8efc6322def5c1b02f074629"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3c03137ee6103ba92d5d6ad6a510e86fded69cd67050bd8a1843f15283be17ac"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be8e298d88f88bd172b59912240558be3b7a04959375646e7fd4996401452941"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:3d407140f5604a89578285d4aac7b18b8eafa055cf776e781aabb89c48738fad"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56e5afb69eb6e1b3ffc34716ee5f92ffbdb5cb003b3a5ca4d4b0fe700e217162"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:27b19b4a286b3ff52040efa42dbe403730aebe5fdfd2def704eb285e2125c63e"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08a63935c66488511b7b29b06233be0be5f4123622fc8fd488f28dc1b7e4c164"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93bd0afcc4cc05794507084afaefb161c3639f283ee629bd0e8654b5c0327ba8"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f3d4da470cfd5cf44f6d682c6eb01363066e0af53ebe111225e44a618f9453d"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba6c12d0e6a67399f4102b4980f4f1bebdbf226ed0a68e84617709d4009b4e71"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b557071405b4aeeaa7cbec1a95d15d6c8f37622fe3f4b595311e0e226ce772c"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cdb406001474726a47fbe903f3aba0de86f5c0b9c9861f55c09c366368225ae0"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6072876ba56446d9ac29d349983677d6f44c6d1c6c1c6be44e66e377c57c767"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c3784c965c9a6822d315d099c3a85b0884ac648952815891c667b469116f1d0"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cc537ad78981df1a827773069fd3b7774f4478db43f518b1616efaf87d7d8f9"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:574ee9dfdc632db8bf9237f27f2a687d1a0b90d29d5e96cab2b21fd2b419c17d"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6594efbaea72dc58b368b53e745ad902c8d8cc41286f00b3743ceac464d5ef3f"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7c849f9ddaf3c7faba938440f9c849235a2908b303063d49da3092a93acd695b"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1fef0296fb3577d0a08ad9b70344ee418f728f1ec21a768ffe774437d67ac859"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1dce1e66fdf72cc474367bd7a7f2b90ec67bb8197dc3fe8ecd08f4ce3ab950a1"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:202fe9975efaec0085cab14a6a6050418bc041f5316f2cf098c0cd2aced4c50e"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:528349434601b9d55e65c6a495494de0001c9a06b431547fea4c60b5edc7d5b3"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3e248cdbf2a54bafdadf4486ddd32e8352f816d3caa2014e44de99f8c525d4a8"}, + {file = "cytoolz-1.1.0-cp39-cp39-win32.whl", hash = "sha256:e63f2b70f4654648a5c6a176ae80897c0de6401f385540dce8e365019e800cfe"}, + {file = "cytoolz-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:f731c53ed29959f105ae622b62e39603c207ed8e8cb2a40cd4accb63d9f92901"}, + {file = "cytoolz-1.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:5a2120bf9e6e8f25e1b32748424a5571e319ef03a995a8fde663fd2feec1a696"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f32e93a55681d782fc6af939f6df36509d65122423cbc930be39b141064adff8"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5d9bc596751cbda8073e65be02ca11706f00029768fbbbc81e11a8c290bb41aa"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b16660d01c3931951fab49db422c627897c38c1a1f0393a97582004019a4887"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b7de5718e2113d4efccea3f06055758cdbc17388ecc3341ba4d1d812837d7c1a"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a12a2a1a6bc44099491c05a12039efa08cc33a3d0f8c7b0566185e085e139283"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:047defa7f5f9a32f82373dbc3957289562e8a3fa58ae02ec8e4dca4f43a33a21"}, + {file = "cytoolz-1.1.0.tar.gz", hash = "sha256:13a7bf254c3c0d28b12e2290b82aed0f0977a4c2a2bf84854fcdc7796a29f3b0"}, ] [package.dependencies] toolz = ">=0.8.0" [package.extras] -cython = ["cython"] +cython = ["cython (>=0.29)"] +test = ["pytest"] [[package]] name = "distlib" @@ -836,67 +831,59 @@ files = [ [[package]] name = "docutils" -version = "0.19" +version = "0.22.4" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, - {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, + {file = "docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"}, + {file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"}, ] [[package]] name = "eth-abi" -version = "4.0.0" +version = "6.0.0" description = "eth_abi: Python utilities for working with Ethereum ABI definitions, especially encoding and decoding" optional = false -python-versions = ">=3.7, <4" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "eth_abi-4.0.0-py3-none-any.whl", hash = "sha256:79d258669f3505319e53638d644a75e1c816db552a1ab1927c3063763cc41031"}, - {file = "eth_abi-4.0.0.tar.gz", hash = "sha256:6949baba61a2c453f0719309ca145e8876a1cbae7ba377c991e67240c13ec7fc"}, + {file = "eth_abi-6.0.0-py3-none-any.whl", hash = "sha256:05ad63b50bd5448cfac0079b3cc5464de7464b4336a2496bf368d5808c7f9e9f"}, + {file = "eth_abi-6.0.0.tar.gz", hash = "sha256:e83a0ed91f2dadeeb50236d673736fe2edc6fcc0a1c1e13d461192d4b23d5bcc"}, ] [package.dependencies] eth-typing = ">=3.0.0" eth-utils = ">=2.0.0" -parsimonious = ">=0.9.0,<0.10.0" +parsimonious = ">=0.10.0,<0.11.0" [package.extras] -dev = ["black", "bumpversion (>=0.5.3,<1)", "eth-hash[pycryptodome]", "flake8", "hypothesis (>=4.18.2,<5.0.0)", "ipython", "isort (>=4.2.15,<5)", "jinja2 (>=3.0.0,<3.1.0)", "mypy (==0.910)", "pydocstyle (>=6.0.0,<7)", "pytest (>=6.2.5,<7)", "pytest-pythonpath (>=0.7.1)", "pytest-watch (>=4.1.0,<5)", "pytest-xdist (>=2.5.0,<3)", "sphinx (>=4.5.0,<5)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (==18.5.0)", "tox (>=2.9.1,<3)", "twine", "wheel"] -doc = ["jinja2 (>=3.0.0,<3.1.0)", "sphinx (>=4.5.0,<5)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (==18.5.0)"] -lint = ["black", "flake8", "isort (>=4.2.15,<5)", "mypy (==0.910)", "pydocstyle (>=6.0.0,<7)"] -test = ["eth-hash[pycryptodome]", "hypothesis (>=4.18.2,<5.0.0)", "pytest (>=6.2.5,<7)", "pytest-pythonpath (>=0.7.1)", "pytest-xdist (>=2.5.0,<3)", "tox (>=2.9.1,<3)"] -tools = ["hypothesis (>=4.18.2,<5.0.0)"] +tools = ["hypothesis (>=6.22.0,<6.108.7)"] [[package]] name = "eth-account" -version = "0.8.0" +version = "0.14.0" description = "eth-account: Sign Ethereum transactions and messages with local private keys" optional = false -python-versions = ">=3.6, <4" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "eth-account-0.8.0.tar.gz", hash = "sha256:ccb2d90a16c81c8ea4ca4dc76a70b50f1d63cea6aff3c5a5eddedf9e45143eca"}, - {file = "eth_account-0.8.0-py3-none-any.whl", hash = "sha256:0ccc0edbb17021004356ae6e37887528b6e59e6ae6283f3917b9759a5887203b"}, + {file = "eth_account-0.14.0-py3-none-any.whl", hash = "sha256:efdcb57f32f133e9152510e44772a4bcfe519317dfd8f7e7c5ead8189f73a2b6"}, + {file = "eth_account-0.14.0.tar.gz", hash = "sha256:2c8291b1a8fcbd29a55b07f75f0a0aaffd04704d3ec28d1396587f18a5541c6d"}, ] [package.dependencies] -bitarray = ">=2.4.0,<3" -eth-abi = ">=3.0.1" -eth-keyfile = ">=0.6.0,<0.7.0" -eth-keys = ">=0.4.0,<0.5" -eth-rlp = ">=0.3.0,<1" -eth-utils = ">=2.0.0,<3" -hexbytes = ">=0.1.0,<1" -rlp = ">=1.0.0,<4" - -[package.extras] -dev = ["Sphinx (>=1.6.5,<5)", "black (>=22,<23)", "bumpversion (>=0.5.3,<1)", "coverage", "flake8 (==3.7.9)", "hypothesis (>=4.18.0,<5)", "ipython", "isort (>=4.2.15,<5)", "jinja2 (>=3.0.0,<3.1.0)", "mypy (==0.910)", "pydocstyle (>=5.0.0,<6)", "pytest (>=6.2.5,<7)", "pytest-watch (>=4.1.0,<5)", "pytest-xdist", "sphinx-rtd-theme (>=0.1.9,<1)", "towncrier (>=21,<22)", "tox (==3.25.0)", "twine", "wheel"] -doc = ["Sphinx (>=1.6.5,<5)", "jinja2 (>=3.0.0,<3.1.0)", "sphinx-rtd-theme (>=0.1.9,<1)", "towncrier (>=21,<22)"] -lint = ["black (>=22,<23)", "flake8 (==3.7.9)", "isort (>=4.2.15,<5)", "mypy (==0.910)", "pydocstyle (>=5.0.0,<6)"] -test = ["coverage", "hypothesis (>=4.18.0,<5)", "pytest (>=6.2.5,<7)", "pytest-xdist", "tox (==3.25.0)"] +bitarray = ">=2.4" +ckzg = ">=2" +eth-abi = ">=4" +eth-keyfile = ">=0.10" +eth-keys = ">=0.5" +eth-rlp = ">=3" +eth-utils = ">=6" +hexbytes = ">=2" +pydantic = ">=2" +rlp = ">=2" [[package]] name = "eth-hash" @@ -923,115 +910,101 @@ test = ["pytest (>=6.2.5,<7)", "pytest-xdist (>=2.4.0,<3)", "tox (>=3.14.6,<4)"] [[package]] name = "eth-keyfile" -version = "0.6.1" -description = "A library for handling the encrypted keyfiles used to store ethereum private keys." +version = "0.10.0" +description = "A library for handling the encrypted keyfiles used to store ethereum private keys" optional = false -python-versions = "*" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "eth-keyfile-0.6.1.tar.gz", hash = "sha256:471be6e5386fce7b22556b3d4bde5558dbce46d2674f00848027cb0a20abdc8c"}, - {file = "eth_keyfile-0.6.1-py3-none-any.whl", hash = "sha256:609773a1ad5956944a33348413cad366ec6986c53357a806528c8f61c4961560"}, + {file = "eth_keyfile-0.10.0-py3-none-any.whl", hash = "sha256:6b8b1e2528ecd3c53f9f733c061a8ddc7aa40b689f15c2f38b73ee6fc5d274fd"}, + {file = "eth_keyfile-0.10.0.tar.gz", hash = "sha256:3003b20000d68203e8fbf45456851a524f859a7432d2fae73be4a9aebeb4b8e1"}, ] [package.dependencies] -eth-keys = ">=0.4.0,<0.5.0" -eth-utils = ">=2,<3" +eth-keys = ">=0.8.0" +eth-utils = ">=2" +py_ecc = ">=5.2.0" pycryptodome = ">=3.6.6,<4" -[package.extras] -dev = ["bumpversion (>=0.5.3,<1)", "eth-keys (>=0.4.0,<0.5.0)", "eth-utils (>=2,<3)", "flake8 (==4.0.1)", "idna (==2.7)", "pluggy (>=1.0.0,<2)", "pycryptodome (>=3.6.6,<4)", "pytest (>=6.2.5,<7)", "requests (>=2.20,<3)", "setuptools (>=38.6.0)", "tox (>=2.7.0)", "twine", "wheel"] -keyfile = ["eth-keys (>=0.4.0,<0.5.0)", "eth-utils (>=2,<3)", "pycryptodome (>=3.6.6,<4)"] -lint = ["flake8 (==4.0.1)"] -test = ["pytest (>=6.2.5,<7)"] - [[package]] name = "eth-keys" -version = "0.4.0" -description = "Common API for Ethereum key operations." +version = "0.8.0" +description = "eth-keys: Common API for Ethereum key operations" optional = false -python-versions = "*" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "eth-keys-0.4.0.tar.gz", hash = "sha256:7d18887483bc9b8a3fdd8e32ddcb30044b9f08fcb24a380d93b6eee3a5bb3216"}, - {file = "eth_keys-0.4.0-py3-none-any.whl", hash = "sha256:e07915ffb91277803a28a379418bdd1fad1f390c38ad9353a0f189789a440d5d"}, + {file = "eth_keys-0.8.0-py3-none-any.whl", hash = "sha256:a7b94222638cccbdf2b5dae5c365d883a96826d82bb0faeb56baa65375f514ae"}, + {file = "eth_keys-0.8.0.tar.gz", hash = "sha256:11549b251876fccd7caedd6905e494ea2309aec352ec2579b00ef9978017a964"}, ] [package.dependencies] -eth-typing = ">=3.0.0,<4" -eth-utils = ">=2.0.0,<3.0.0" +eth-typing = ">=3" +eth-utils = ">=2" [package.extras] -coincurve = ["coincurve (>=7.0.0,<16.0.0)"] -dev = ["asn1tools (>=0.146.2,<0.147)", "bumpversion (==0.5.3)", "eth-hash[pycryptodome] ; implementation_name == \"pypy\"", "eth-hash[pysha3] ; implementation_name == \"cpython\"", "eth-typing (>=3.0.0,<4)", "eth-utils (>=2.0.0,<3.0.0)", "factory-boy (>=3.0.1,<3.1)", "flake8 (==3.0.4)", "hypothesis (>=5.10.3,<6.0.0)", "mypy (==0.782)", "pyasn1 (>=0.4.5,<0.5)", "pytest (==6.2.5)", "tox (==3.20.0)", "twine"] -eth-keys = ["eth-typing (>=3.0.0,<4)", "eth-utils (>=2.0.0,<3.0.0)"] -lint = ["flake8 (==3.0.4)", "mypy (==0.782)"] -test = ["asn1tools (>=0.146.2,<0.147)", "eth-hash[pycryptodome] ; implementation_name == \"pypy\"", "eth-hash[pysha3] ; implementation_name == \"cpython\"", "factory-boy (>=3.0.1,<3.1)", "hypothesis (>=5.10.3,<6.0.0)", "pyasn1 (>=0.4.5,<0.5)", "pytest (==6.2.5)"] +coincurve = ["coincurve (>=21.0.0) ; python_version < \"3.14\""] [[package]] name = "eth-rlp" -version = "0.3.0" +version = "3.0.0" description = "eth-rlp: RLP definitions for common Ethereum objects in Python" optional = false -python-versions = ">=3.7, <4" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "eth-rlp-0.3.0.tar.gz", hash = "sha256:f3263b548df718855d9a8dbd754473f383c0efc82914b0b849572ce3e06e71a6"}, - {file = "eth_rlp-0.3.0-py3-none-any.whl", hash = "sha256:e88e949a533def85c69fa94224618bbbd6de00061f4cff645c44621dab11cf33"}, + {file = "eth_rlp-3.0.0-py3-none-any.whl", hash = "sha256:32f355261c36ad2c369db098170f873123795667e50d496b034f369e2c9d2346"}, + {file = "eth_rlp-3.0.0.tar.gz", hash = "sha256:9663e54a4a1c1c847d2d328c1d07e4174ec1c082953fbb42b60e61c501c4931c"}, ] [package.dependencies] -eth-utils = ">=2.0.0,<3" -hexbytes = ">=0.1.0,<1" -rlp = ">=0.6.0,<4" - -[package.extras] -dev = ["Sphinx (>=1.6.5,<2)", "bumpversion (>=0.5.3,<1)", "eth-hash[pycryptodome]", "flake8 (==3.7.9)", "ipython", "isort (>=4.2.15,<5)", "mypy (==0.770)", "pydocstyle (>=3.0.0,<4)", "pytest (>=6.2.5,<7)", "pytest-watch (>=4.1.0,<5)", "pytest-xdist", "sphinx-rtd-theme (>=0.1.9)", "towncrier (>=19.2.0,<20)", "tox (==3.14.6)", "twine", "wheel"] -doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)", "towncrier (>=19.2.0,<20)"] -lint = ["flake8 (==3.7.9)", "isort (>=4.2.15,<5)", "mypy (==0.770)", "pydocstyle (>=3.0.0,<4)"] -test = ["eth-hash[pycryptodome]", "pytest (>=6.2.5,<7)", "pytest-xdist", "tox (==3.14.6)"] +eth-utils = ">=2.0.0" +hexbytes = ">=1.2.0" +rlp = ">=3.0.0" [[package]] name = "eth-typing" -version = "3.3.0" +version = "6.0.0" description = "eth-typing: Common type annotations for ethereum python packages" optional = false -python-versions = ">=3.7.2, <4" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "eth-typing-3.3.0.tar.gz", hash = "sha256:e9535e9d524d4c7a0cbd3d9832093cc5001a3e31869e72645674d24c6376d196"}, - {file = "eth_typing-3.3.0-py3-none-any.whl", hash = "sha256:323111b3b76c8ceaff01619367aa52806f0264ca0ec1a70d4b9a42e44360f554"}, + {file = "eth_typing-6.0.0-py3-none-any.whl", hash = "sha256:ee74fb641eb36dd885e1c42c2a3055314efa532b3e71480816df70a94d35cfb9"}, + {file = "eth_typing-6.0.0.tar.gz", hash = "sha256:315dd460dc0b71c15a6cd51e3c0b70d237eec8771beb844144f3a1fb4adb2392"}, ] +[package.dependencies] +typing_extensions = ">=4.5.0" + [package.extras] -dev = ["bumpversion (>=0.5.3,<1)", "flake8 (==3.8.3)", "ipython", "isort (>=4.2.15,<5)", "mypy (==0.910)", "pydocstyle (>=3.0.0,<4)", "pytest (>=6.2.5,<7)", "pytest-watch (>=4.1.0,<5)", "pytest-xdist", "sphinx (>=4.2.0,<5)", "sphinx-rtd-theme (>=0.1.9)", "towncrier (>=21,<22)", "tox (>=2.9.1,<3)", "twine", "wheel"] -doc = ["sphinx (>=4.2.0,<5)", "sphinx-rtd-theme (>=0.1.9)", "towncrier (>=21,<22)"] -lint = ["flake8 (==3.8.3)", "isort (>=4.2.15,<5)", "mypy (==0.910)", "pydocstyle (>=3.0.0,<4)"] -test = ["pytest (>=6.2.5,<7)", "pytest-xdist", "tox (>=2.9.1,<3)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.18.2)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel (>=0.38.1)"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-utils" -version = "2.1.0" +version = "6.0.0" description = "eth-utils: Common utility functions for python code that interacts with Ethereum" optional = false -python-versions = ">=3.7,<4" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "eth-utils-2.1.0.tar.gz", hash = "sha256:fcb4c3c1b32947ba92970963f9aaf40da73b04ea1034964ff8c0e70595127138"}, - {file = "eth_utils-2.1.0-py3-none-any.whl", hash = "sha256:63901e54ec9e4ac16ae0a0d28e1dc48b968c20184d22f2727e5f3ca24b6250bc"}, + {file = "eth_utils-6.0.0-py3-none-any.whl", hash = "sha256:63cf48ee32c45541cb5748751909a8345c470432fb6f0fed4bd7c53fd6400469"}, + {file = "eth_utils-6.0.0.tar.gz", hash = "sha256:eb54b2f82dd300d3142c49a89da195e823f5e5284d43203593f87c67bad92a96"}, ] [package.dependencies] -cached-property = {version = ">=1.5.2,<2", markers = "python_version < \"3.8\""} cytoolz = {version = ">=0.10.1", markers = "implementation_name == \"cpython\""} eth-hash = ">=0.3.1" -eth-typing = ">=3.0.0" +eth-typing = ">=5.0.0" +pydantic = ">=2.0.0,<3" toolz = {version = ">0.8.2", markers = "implementation_name == \"pypy\""} [package.extras] -dev = ["Sphinx (>=1.6.5,<2)", "black (>=22)", "bumpversion (>=0.5.3,<1)", "flake8 (==3.7.9)", "hypothesis (>=4.43.0,<5.0.0)", "ipython", "isort (>=4.2.15,<5)", "jinja2 (>=3.0.0,<3.0.1)", "mypy (==0.910)", "pydocstyle (>=5.0.0,<6)", "pytest (>=6.2.5,<7)", "pytest-watch (>=4.1.0,<5)", "pytest-xdist", "sphinx-rtd-theme (>=0.1.9,<2)", "towncrier (>=21,<22)", "tox (==3.14.6)", "twine (>=1.13,<2)", "types-setuptools", "wheel (>=0.30.0,<1.0.0)"] -doc = ["Sphinx (>=1.6.5,<2)", "jinja2 (>=3.0.0,<3.0.1)", "sphinx-rtd-theme (>=0.1.9,<2)", "towncrier (>=21,<22)"] -lint = ["black (>=22)", "flake8 (==3.7.9)", "isort (>=4.2.15,<5)", "mypy (==0.910)", "pydocstyle (>=5.0.0,<6)", "pytest (>=6.2.5,<7)", "types-setuptools"] -test = ["hypothesis (>=4.43.0,<5.0.0)", "pytest (>=6.2.5,<7)", "pytest-xdist", "tox (==3.14.6)", "types-setuptools"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=4.43.0)", "ipython", "mypy (==1.18.2)", "mypy (==1.18.2)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel (>=0.38.1)"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["hypothesis (>=4.43.0)", "mypy (==1.18.2)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "filelock" @@ -1049,126 +1022,158 @@ files = [ docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] -[[package]] -name = "flake8" -version = "3.9.2" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -groups = ["dev"] -files = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" - [[package]] name = "frozenlist" -version = "1.3.3" +version = "1.8.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, - {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, - {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, - {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, - {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, - {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, - {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, - {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, - {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, - {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, - {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, - {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, - {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, - {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, - {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, ] [[package]] name = "hexbytes" -version = "0.3.0" +version = "2.0.0" description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" optional = false -python-versions = ">=3.7, <4" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "hexbytes-0.3.0-py3-none-any.whl", hash = "sha256:21c3a5bd00a383097f0369c387174e79839d75c4ccc3a7edda315c9644f4458a"}, - {file = "hexbytes-0.3.0.tar.gz", hash = "sha256:afeebfb800f5f15a3ca5bab52e49eabcb4b6dac06ec8ff01a94fdb890c6c0712"}, + {file = "hexbytes-2.0.0-py3-none-any.whl", hash = "sha256:5425bd7ac83cdd9791c13a5bf97cfe9b9609a304b1ef3ab146adfd50de06cf0e"}, + {file = "hexbytes-2.0.0.tar.gz", hash = "sha256:01312fcd5c57e8a8d2d7dd3274dcf84ea50422aff2abcc2d9fd89ad6a32498e5"}, ] -[package.extras] -dev = ["Sphinx (>=4.0.0,<5)", "black (>=22,<23)", "bumpversion (>=0.5.3,<1)", "eth-utils (>=1.0.1,<3)", "flake8 (==3.7.9)", "hypothesis (>=3.44.24,<=6.31.6)", "ipython", "isort (>=4.2.15,<5)", "mypy (==0.971)", "pydocstyle (>=5.0.0,<6)", "pytest (>=7,<8)", "pytest-watch (>=4.1.0,<5)", "pytest-xdist", "sphinx-rtd-theme (>=0.1.9,<1)", "towncrier (>=21,<22)", "tox (>=3.25.1,<4)", "twine", "wheel"] -doc = ["Sphinx (>=4.0.0,<5)", "sphinx-rtd-theme (>=0.1.9,<1)", "towncrier (>=21,<22)"] -lint = ["black (>=22,<23)", "flake8 (==3.7.9)", "isort (>=4.2.15,<5)", "mypy (==0.971)", "pydocstyle (>=5.0.0,<6)"] -test = ["eth-utils (>=1.0.1,<3)", "hypothesis (>=3.44.24,<=6.31.6)", "pytest (>=7,<8)", "pytest-xdist", "tox (>=3.25.1,<4)"] - [[package]] name = "identify" version = "2.5.24" @@ -1208,48 +1213,6 @@ files = [ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -[[package]] -name = "importlib-metadata" -version = "6.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, - {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, -] -markers = {main = "python_version < \"3.8\"", dev = "python_version < \"3.10\""} - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[[package]] -name = "importlib-resources" -version = "5.12.0" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.7" -groups = ["main"] -markers = "python_version < \"3.9\"" -files = [ - {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, - {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8 ; python_version < \"3.12\"", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\""] - [[package]] name = "iniconfig" version = "2.0.0" @@ -1280,91 +1243,6 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] -[[package]] -name = "jsonschema" -version = "4.17.3" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, - {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, -] - -[package.dependencies] -attrs = ">=17.4.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} -pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""} -pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - -[[package]] -name = "lru-dict" -version = "1.1.8" -description = "An Dict like LRU container." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "lru-dict-1.1.8.tar.gz", hash = "sha256:878bc8ef4073e5cfb953dfc1cf4585db41e8b814c0106abde34d00ee0d0b3115"}, - {file = "lru_dict-1.1.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9d5815c0e85922cd0fb8344ca8b1c7cf020bf9fc45e670d34d51932c91fd7ec"}, - {file = "lru_dict-1.1.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f877f53249c3e49bbd7612f9083127290bede6c7d6501513567ab1bf9c581381"}, - {file = "lru_dict-1.1.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fef595c4f573141d54a38bda9221b9ee3cbe0acc73d67304a1a6d5972eb2a02"}, - {file = "lru_dict-1.1.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:db20597c4e67b4095b376ce2e83930c560f4ce481e8d05737885307ed02ba7c1"}, - {file = "lru_dict-1.1.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5b09dbe47bc4b4d45ffe56067aff190bc3c0049575da6e52127e114236e0a6a7"}, - {file = "lru_dict-1.1.8-cp310-cp310-win32.whl", hash = "sha256:3b1692755fef288b67af5cd8a973eb331d1f44cb02cbdc13660040809c2bfec6"}, - {file = "lru_dict-1.1.8-cp310-cp310-win_amd64.whl", hash = "sha256:8f6561f9cd5a452cb84905c6a87aa944fdfdc0f41cc057d03b71f9b29b2cc4bd"}, - {file = "lru_dict-1.1.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ca8f89361e0e7aad0bf93ae03a31502e96280faeb7fb92267f4998fb230d36b2"}, - {file = "lru_dict-1.1.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c50ab9edaa5da5838426816a2b7bcde9d576b4fc50e6a8c062073dbc4969d78"}, - {file = "lru_dict-1.1.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fe16ade5fd0a57e9a335f69b8055aaa6fb278fbfa250458e4f6b8255115578f"}, - {file = "lru_dict-1.1.8-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:de972c7f4bc7b6002acff2a8de984c55fbd7f2289dba659cfd90f7a0f5d8f5d1"}, - {file = "lru_dict-1.1.8-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3d003a864899c29b0379e412709a6e516cbd6a72ee10b09d0b33226343617412"}, - {file = "lru_dict-1.1.8-cp36-cp36m-win32.whl", hash = "sha256:6e2a7aa9e36626fb48fdc341c7e3685a31a7b50ea4918677ea436271ad0d904d"}, - {file = "lru_dict-1.1.8-cp36-cp36m-win_amd64.whl", hash = "sha256:d2ed4151445c3f30423c2698f72197d64b27b1cd61d8d56702ffe235584e47c2"}, - {file = "lru_dict-1.1.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:075b9dd46d7022b675419bc6e3631748ae184bc8af195d20365a98b4f3bb2914"}, - {file = "lru_dict-1.1.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70364e3cbef536adab8762b4835e18f5ca8e3fddd8bd0ec9258c42bbebd0ee77"}, - {file = "lru_dict-1.1.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:720f5728e537f11a311e8b720793a224e985d20e6b7c3d34a891a391865af1a2"}, - {file = "lru_dict-1.1.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c2fe692332c2f1d81fd27457db4b35143801475bfc2e57173a2403588dd82a42"}, - {file = "lru_dict-1.1.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:86d32a4498b74a75340497890a260d37bf1560ad2683969393032977dd36b088"}, - {file = "lru_dict-1.1.8-cp37-cp37m-win32.whl", hash = "sha256:348167f110494cfafae70c066470a6f4e4d43523933edf16ccdb8947f3b5fae0"}, - {file = "lru_dict-1.1.8-cp37-cp37m-win_amd64.whl", hash = "sha256:9be6c4039ef328676b868acea619cd100e3de1a35b3be211cf0eaf9775563b65"}, - {file = "lru_dict-1.1.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a777d48319d293b1b6a933d606c0e4899690a139b4c81173451913bbcab6f44f"}, - {file = "lru_dict-1.1.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99f6cfb3e28490357a0805b409caf693e46c61f8dbb789c51355adb693c568d3"}, - {file = "lru_dict-1.1.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:163079dbda54c3e6422b23da39fb3ecc561035d65e8496ff1950cbdb376018e1"}, - {file = "lru_dict-1.1.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0972d669e9e207617e06416166718b073a49bf449abbd23940d9545c0847a4d9"}, - {file = "lru_dict-1.1.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:97c24ffc55de6013075979f440acd174e88819f30387074639fb7d7178ca253e"}, - {file = "lru_dict-1.1.8-cp38-cp38-win32.whl", hash = "sha256:0f83cd70a6d32f9018d471be609f3af73058f700691657db4a3d3dd78d3f96dd"}, - {file = "lru_dict-1.1.8-cp38-cp38-win_amd64.whl", hash = "sha256:add762163f4af7f4173fafa4092eb7c7f023cf139ef6d2015cfea867e1440d82"}, - {file = "lru_dict-1.1.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ac524e4615f06dc72ffbfd83f26e073c9ec256de5413634fbd024c010a8bc"}, - {file = "lru_dict-1.1.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7284bdbc5579bbdc3fc8f869ed4c169f403835566ab0f84567cdbfdd05241847"}, - {file = "lru_dict-1.1.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ca497cb25f19f24171f9172805f3ff135b911aeb91960bd4af8e230421ccb51"}, - {file = "lru_dict-1.1.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1df1da204a9f0b5eb8393a46070f1d984fa8559435ee790d7f8f5602038fc00"}, - {file = "lru_dict-1.1.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f4d0a6d733a23865019b1c97ed6fb1fdb739be923192abf4dbb644f697a26a69"}, - {file = "lru_dict-1.1.8-cp39-cp39-win32.whl", hash = "sha256:7be1b66926277993cecdc174c15a20c8ce785c1f8b39aa560714a513eef06473"}, - {file = "lru_dict-1.1.8-cp39-cp39-win_amd64.whl", hash = "sha256:881104711900af45967c2e5ce3e62291dd57d5b2a224d58b7c9f60bf4ad41b8c"}, - {file = "lru_dict-1.1.8-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:beb089c46bd95243d1ac5b2bd13627317b08bf40dd8dc16d4b7ee7ecb3cf65ca"}, - {file = "lru_dict-1.1.8-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10fe823ff90b655f0b6ba124e2b576ecda8c61b8ead76b456db67831942d22f2"}, - {file = "lru_dict-1.1.8-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07163c9dcbb2eca377f366b1331f46302fd8b6b72ab4d603087feca00044bb0"}, - {file = "lru_dict-1.1.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93336911544ebc0e466272043adab9fb9f6e9dcba6024b639c32553a3790e089"}, - {file = "lru_dict-1.1.8-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:55aeda6b6789b2d030066b4f5f6fc3596560ba2a69028f35f3682a795701b5b1"}, - {file = "lru_dict-1.1.8-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:262a4e622010ceb960a6a5222ed011090e50954d45070fd369c0fa4d2ed7d9a9"}, - {file = "lru_dict-1.1.8-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6f64005ede008b7a866be8f3f6274dbf74e656e15e4004e9d99ad65efb01809"}, - {file = "lru_dict-1.1.8-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:9d70257246b8207e8ef3d8b18457089f5ff0dfb087bd36eb33bce6584f2e0b3a"}, - {file = "lru_dict-1.1.8-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f874e9c2209dada1a080545331aa1277ec060a13f61684a8642788bf44b2325f"}, - {file = "lru_dict-1.1.8-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a592363c93d6fc6472d5affe2819e1c7590746aecb464774a4f67e09fbefdfc"}, - {file = "lru_dict-1.1.8-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f340b61f3cdfee71f66da7dbfd9a5ea2db6974502ccff2065cdb76619840dca"}, - {file = "lru_dict-1.1.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9447214e4857e16d14158794ef01e4501d8fad07d298d03308d9f90512df02fa"}, -] - -[package.extras] -test = ["pytest"] - [[package]] name = "markupsafe" version = "2.1.2" @@ -1425,100 +1303,160 @@ files = [ {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, ] -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] - [[package]] name = "multidict" -version = "6.0.4" +version = "6.7.1" description = "multidict implementation" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, ] [[package]] @@ -1559,8 +1497,6 @@ files = [ [package.dependencies] mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} typing-extensions = ">=3.10" [package.extras] @@ -1607,43 +1543,19 @@ files = [ [[package]] name = "parsimonious" -version = "0.9.0" +version = "0.10.0" description = "(Soon to be) the fastest pure-Python PEG parser I could muster" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "parsimonious-0.9.0.tar.gz", hash = "sha256:b2ad1ae63a2f65bd78f5e0a8ac510a98f3607a43f1db2a8d46636a5d9e4a30c1"}, + {file = "parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f"}, + {file = "parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c"}, ] [package.dependencies] regex = ">=2022.3.15" -[[package]] -name = "pathspec" -version = "0.11.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, -] - -[[package]] -name = "pkgutil-resolve-name" -version = "1.3.10" -description = "Resolve a name to an object." -optional = false -python-versions = ">=3.6" -groups = ["main"] -markers = "python_version < \"3.9\"" -files = [ - {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"}, - {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, -] - [[package]] name = "platformdirs" version = "3.2.0" @@ -1656,31 +1568,25 @@ files = [ {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.5", markers = "python_version < \"3.8\""} - [package.extras] docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pre-commit" @@ -1697,57 +1603,161 @@ files = [ [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] -name = "protobuf" -version = "4.22.1" -description = "" +name = "propcache" +version = "0.5.2" +description = "Accelerated property cache" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "protobuf-4.22.1-cp310-abi3-win32.whl", hash = "sha256:85aa9acc5a777adc0c21b449dafbc40d9a0b6413ff3a4f77ef9df194be7f975b"}, - {file = "protobuf-4.22.1-cp310-abi3-win_amd64.whl", hash = "sha256:8bc971d76c03f1dd49f18115b002254f2ddb2d4b143c583bb860b796bb0d399e"}, - {file = "protobuf-4.22.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:5917412347e1da08ce2939eb5cd60650dfb1a9ab4606a415b9278a1041fb4d19"}, - {file = "protobuf-4.22.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9e12e2810e7d297dbce3c129ae5e912ffd94240b050d33f9ecf023f35563b14f"}, - {file = "protobuf-4.22.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:953fc7904ef46900262a26374b28c2864610b60cdc8b272f864e22143f8373c4"}, - {file = "protobuf-4.22.1-cp37-cp37m-win32.whl", hash = "sha256:6e100f7bc787cd0a0ae58dbf0ab8bbf1ee7953f862b89148b6cf5436d5e9eaa1"}, - {file = "protobuf-4.22.1-cp37-cp37m-win_amd64.whl", hash = "sha256:87a6393fa634f294bf24d1cfe9fdd6bb605cbc247af81b9b10c4c0f12dfce4b3"}, - {file = "protobuf-4.22.1-cp38-cp38-win32.whl", hash = "sha256:e3fb58076bdb550e75db06ace2a8b3879d4c4f7ec9dd86e4254656118f4a78d7"}, - {file = "protobuf-4.22.1-cp38-cp38-win_amd64.whl", hash = "sha256:651113695bc2e5678b799ee5d906b5d3613f4ccfa61b12252cfceb6404558af0"}, - {file = "protobuf-4.22.1-cp39-cp39-win32.whl", hash = "sha256:67b7d19da0fda2733702c2299fd1ef6cb4b3d99f09263eacaf1aa151d9d05f02"}, - {file = "protobuf-4.22.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8700792f88e59ccecfa246fa48f689d6eee6900eddd486cdae908ff706c482b"}, - {file = "protobuf-4.22.1-py3-none-any.whl", hash = "sha256:3e19dcf4adbf608924d3486ece469dd4f4f2cf7d2649900f0efcd1a84e8fd3ba"}, - {file = "protobuf-4.22.1.tar.gz", hash = "sha256:dce7a55d501c31ecf688adb2f6c3f763cf11bc0be815d1946a84d74772ab07a7"}, -] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -groups = ["dev"] + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, + {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, + {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, + {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, + {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, + {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, + {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, + {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, + {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, + {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, + {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, + {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, + {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, + {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, + {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, + {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, + {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, + {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, + {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, + {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, + {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, + {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, + {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, + {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, +] + +[[package]] +name = "py-ecc" +version = "8.0.0" +description = "py-ecc: Elliptic curve crypto in python including secp256k1, alt_bn128, and bls12_381" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, + {file = "py_ecc-8.0.0-py3-none-any.whl", hash = "sha256:c0b2dfc4bde67a55122a392591a10e851a986d5128f680628c80b405f7663e13"}, + {file = "py_ecc-8.0.0.tar.gz", hash = "sha256:56aca19e5dc37294f60c1cc76666c03c2276e7666412b9a559fa0145d099933d"}, ] -[[package]] -name = "pycodestyle" -version = "2.7.0" -description = "Python style guide checker" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] +[package.dependencies] +eth-typing = ">=3.0.0" +eth-utils = ">=2.0.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "pycryptodome" @@ -1792,16 +1802,171 @@ files = [ {file = "pycryptodome-3.17.tar.gz", hash = "sha256:bce2e2d8e82fcf972005652371a3e8731956a0c1fbb719cc897943b3695ad91b"}, ] +[[package]] +name = "pydantic" +version = "2.13.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73"}, + {file = "pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.46.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win32.whl", hash = "sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win_amd64.whl", hash = "sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266"}, + {file = "pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + [[package]] name = "pydata-sphinx-theme" -version = "0.13.1" +version = "0.20.0" description = "Bootstrap-based Sphinx theme from the PyData community" optional = false -python-versions = ">=3.7" +python-versions = ">=3.11" groups = ["dev"] files = [ - {file = "pydata_sphinx_theme-0.13.1-py3-none-any.whl", hash = "sha256:ce29c1de7961d616dfa25f4c3a9619818d4bb2d4a9985ed078367af294fbccc2"}, - {file = "pydata_sphinx_theme-0.13.1.tar.gz", hash = "sha256:a37d967207c2d787cfe5cf74abfc6b3bcaf0606cac6b283a3876e603d67d6a71"}, + {file = "pydata_sphinx_theme-0.20.0-py3-none-any.whl", hash = "sha256:56744483c9d72c783e075de716ab95d486108b69605df7528078090b73f11f69"}, + {file = "pydata_sphinx_theme-0.20.0.tar.gz", hash = "sha256:0da172d41e19a66de875f4002f7054b385372ec65763852193791e658d50bb4a"}, ] [package.dependencies] @@ -1809,104 +1974,74 @@ accessible-pygments = "*" Babel = "*" beautifulsoup4 = "*" docutils = "!=0.17.0" -packaging = "*" +jinja2 = "*" pygments = ">=2.7" -sphinx = ">=4.2" +requests = "*" +sphinx = ">=8.2,<10" [package.extras] -dev = ["nox", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml"] -doc = ["ablog (>=0.11.0rc2)", "colorama", "ipyleaflet", "jupyter_sphinx", "linkify-it-py", "matplotlib", "myst-nb", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap", "sphinx-togglebutton", "sphinxcontrib-youtube", "sphinxext-rediraffe", "xarray"] -test = ["codecov", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "pyflakes" -version = "2.3.1" -description = "passive checker of Python programs" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["dev"] -files = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] +a11y = ["pydata-sphinx-theme[test]"] +dev = ["pandoc", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml", "sphinx-theme-builder[cli]", "tox"] +doc = ["ablog (>=0.11.8)", "astroid (>=3,!=4.0.3)", "colorama", "graphviz", "ipykernel", "ipyleaflet", "ipywidgets", "jupyter_sphinx", "jupyterlite-sphinx", "linkify-it-py", "matplotlib", "myst-parser", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-autoapi (==3.6.1)", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap (<2.7.0)", "sphinx-togglebutton", "sphinxcontrib-mermaid", "sphinxcontrib-youtube (>=1.4.1)", "sphinxext-rediraffe", "xarray"] +i18n = ["Babel", "jinja2"] +test = ["pytest", "pytest-cov", "pytest-playwright", "pytest-regressions", "sphinx[test]", "typing-extensions"] [[package]] name = "pygments" -version = "2.14.0" +version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, - {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] [package.extras] -plugins = ["importlib-metadata ; python_version < \"3.8\""] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] -name = "pyrsistent" -version = "0.19.3" -description = "Persistent/Functional/Immutable data structures" +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" -groups = ["main"] +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"}, - {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"}, - {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"}, - {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"}, - {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"}, - {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"}, - {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"}, - {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"}, - {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"}, - {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"}, - {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"}, - {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"}, - {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"}, - {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"}, - {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"}, - {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"}, - {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"}, - {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"}, - {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"}, - {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"}, - {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"}, - {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"}, - {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + [[package]] -name = "pytest" -version = "6.2.5" -description = "pytest: simple powerful testing with Python" +name = "pytest-asyncio" +version = "1.4.0" +description = "Pytest support for asyncio" optional = false -python-versions = ">=3.6" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, + {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, ] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -toml = "*" +pytest = ">=8.4,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" @@ -1958,19 +2093,6 @@ files = [ [package.extras] cli = ["click (>=5.0)"] -[[package]] -name = "pytz" -version = "2023.3" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -groups = ["dev"] -markers = "python_version < \"3.9\"" -files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, -] - [[package]] name = "pyunormalize" version = "16.0.0" @@ -2169,47 +2291,104 @@ files = [ [[package]] name = "requests" -version = "2.28.2" +version = "2.34.2" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7, <4" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, - {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "rlp" -version = "3.0.0" -description = "A package for Recursive Length Prefix encoding and decoding" +version = "5.0.0" +description = "rlp: A package for Recursive Length Prefix encoding and decoding" optional = false -python-versions = "*" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "rlp-3.0.0-py2.py3-none-any.whl", hash = "sha256:d2a963225b3f26795c5b52310e0871df9824af56823d739511583ef459895a7d"}, - {file = "rlp-3.0.0.tar.gz", hash = "sha256:63b0465d2948cd9f01de449d7adfb92d207c1aef3982f20310f8009be4a507e8"}, + {file = "rlp-5.0.0-py3-none-any.whl", hash = "sha256:9525fb895d3a6540b7401eadd11c1b64740a5eccb82e543ac7e9735d319b0977"}, + {file = "rlp-5.0.0.tar.gz", hash = "sha256:ae8ac791160c160e270f9c7df76e68f4d42bb86a13726d807b9357c312d0bac4"}, ] [package.dependencies] -eth-utils = ">=2.0.0,<3" +eth-utils = ">=2" + +[package.extras] +rust-backend = ["rusty-rlp (>=0.2.1)"] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +description = "Manipulate well-formed Roman numerals" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7"}, + {file = "roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2"}, +] + +[[package]] +name = "ruff" +version = "0.16.0" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e"}, + {file = "ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522"}, + {file = "ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed"}, + {file = "ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb"}, + {file = "ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472"}, + {file = "ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d"}, + {file = "ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982"}, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6"}, + {file = "setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a"}, +] [package.extras] -dev = ["Sphinx (>=1.6.5,<2)", "bumpversion (>=0.5.3,<1)", "flake8 (==3.4.1)", "hypothesis (==5.19.0)", "ipython", "pytest (>=6.2.5,<7)", "pytest-watch (>=4.1.0,<5)", "pytest-xdist", "setuptools (>=36.2.0)", "sphinx-rtd-theme (>=0.1.9)", "tox (>=2.9.1,<3)", "twine", "wheel"] -doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"] -lint = ["flake8 (==3.4.1)"] -rust-backend = ["rusty-rlp (>=0.2.1,<0.3)"] -test = ["hypothesis (==5.19.0)", "pytest (>=6.2.5,<7)", "tox (>=2.9.1,<3)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] [[package]] name = "snowballstemmer" @@ -2237,60 +2416,55 @@ files = [ [[package]] name = "sphinx" -version = "5.3.0" +version = "9.1.0" description = "Python documentation generator" optional = false -python-versions = ">=3.6" +python-versions = ">=3.12" groups = ["dev"] files = [ - {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, - {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, + {file = "sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978"}, + {file = "sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.20" +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.21,<0.23" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.12" -requests = ">=2.5.0" -snowballstemmer = ">=2.0" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast ; python_version < \"3.8\""] +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +roman-numerals = ">=1.0.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" [[package]] name = "sphinx-book-theme" -version = "1.0.0" +version = "1.4.0" description = "A clean book theme for scientific explanations and documentation with Sphinx" optional = false -python-versions = ">=3.7" +python-versions = ">=3.11" groups = ["dev"] files = [ - {file = "sphinx_book_theme-1.0.0-py3-none-any.whl", hash = "sha256:f6baba7888d5a63328a210e9960a7a6e951e9632d74edb6fce48c9d5d8c28768"}, - {file = "sphinx_book_theme-1.0.0.tar.gz", hash = "sha256:7c76ea51347e55d801504bfb1b2433cba49e7212a25fbebd0ce44b3c098ea946"}, + {file = "sphinx_book_theme-1.4.0-py3-none-any.whl", hash = "sha256:10fd57d667ee0b565928c3ce95562e281c5d1b94bd5a2aafc5cd41a5d498dcd0"}, + {file = "sphinx_book_theme-1.4.0.tar.gz", hash = "sha256:fee3cd70573d9a8cd4ab380a3cb1f11139c24dcaa2cf47d06d7f924f1b44eaff"}, ] [package.dependencies] -pydata-sphinx-theme = ">=0.13.0" -sphinx = ">=4,<7" +pydata-sphinx-theme = "0.20.0" +sphinx = ">=8.2" [package.extras] code-style = ["pre-commit"] -doc = ["ablog", "docutils (==0.17.1)", "folium", "ipywidgets", "matplotlib", "myst-nb", "nbclient", "numpy", "numpydoc", "pandas", "plotly", "sphinx-copybutton", "sphinx-design", "sphinx-examples", "sphinx-tabs (<=3.4.0)", "sphinx-thebe", "sphinx-togglebutton", "sphinxcontrib-bibtex", "sphinxcontrib-youtube", "sphinxext-opengraph"] -test = ["beautifulsoup4", "coverage", "myst-nb", "pytest", "pytest-cov", "pytest-regressions", "sphinx_thebe"] +doc = ["ablog (>=0.11.13)", "folium", "ipywidgets", "matplotlib", "myst-nb", "nbclient", "numpy", "numpydoc", "pandas", "plotly", "sphinx-copybutton", "sphinx-design", "sphinx-examples", "sphinx-tabs", "sphinx-thebe", "sphinx-togglebutton", "sphinxcontrib-bibtex", "sphinxcontrib-youtube", "sphinxext-opengraph"] +test = ["beautifulsoup4", "coverage", "defusedxml", "myst-nb", "pytest", "pytest-cov", "pytest-regressions", "sphinx_thebe"] [[package]] name = "sphinx-click" @@ -2311,50 +2485,53 @@ sphinx = ">=2.0" [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.2" -description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.0" +version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] @@ -2374,61 +2551,38 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] [package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] test = ["pytest"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -groups = ["dev"] -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_full_version <= \"3.11.0a6\"" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - [[package]] name = "toolz" version = "0.12.0" @@ -2436,59 +2590,38 @@ description = "List processing tools and functional utilities" optional = false python-versions = ">=3.5" groups = ["main"] -markers = "implementation_name == \"cpython\" or implementation_name == \"pypy\"" files = [ {file = "toolz-0.12.0-py3-none-any.whl", hash = "sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f"}, {file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"}, ] [[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" +name = "typing-extensions" +version = "4.16.0" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.6" -groups = ["dev"] -markers = "python_version < \"3.8\"" -files = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] [[package]] -name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +name = "typing-inspection" +version = "0.4.4" +description = "Runtime typing introspection tools" optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] +python-versions = ">=3.10" +groups = ["main"] files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, + {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, ] +[package.dependencies] +typing-extensions = ">=4.15.0" + [[package]] name = "urllib3" version = "1.26.15" @@ -2521,7 +2654,6 @@ files = [ [package.dependencies] distlib = ">=0.3.6,<1" filelock = ">=3.11,<4" -importlib-metadata = {version = ">=6.4.1", markers = "python_version < \"3.8\""} platformdirs = ">=3.2,<4" [package.extras] @@ -2530,226 +2662,311 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess [[package]] name = "web3" -version = "6.20.4" -description = "web3.py" +version = "8.0.0" +description = "web3: A Python library for interacting with Ethereum" optional = false -python-versions = ">=3.7.2" +python-versions = "<4,>=3.10" groups = ["main"] files = [ - {file = "web3-6.20.4-py3-none-any.whl", hash = "sha256:a6e1c428a54bf0fd398fbf006d00235898aed794476177ce73f7db177d2ad16c"}, - {file = "web3-6.20.4.tar.gz", hash = "sha256:7f3cdceca369be3eb959ce3905783cd021a0786f40c60b01e40ae2ba538e6e3f"}, + {file = "web3-8.0.0-py3-none-any.whl", hash = "sha256:2276762aa5d86807eabad5af5c585a84bd6d9a706a613f8456c645a9669ffeb5"}, + {file = "web3-8.0.0.tar.gz", hash = "sha256:8d898fe1909037d6269c998d27c1806ce201cf6d7e49514ad6f3e4dcc64e6e9a"}, ] [package.dependencies] aiohttp = ">=3.7.4.post0" -ckzg = "<2" -eth-abi = ">=4.0.0" -eth-account = ">=0.8.0,<0.13" +eth-abi = ">=6" +eth-account = ">=0.14" eth-hash = {version = ">=0.5.1", extras = ["pycryptodome"]} -eth-typing = ">=3.0.0,<4.2.0 || >4.2.0,<5.0.0" -eth-utils = ">=2.1.0,<5" -hexbytes = ">=0.1.0,<0.4.0" -jsonschema = ">=4.0.0" -lru-dict = ">=1.1.6,<1.3.0" -protobuf = ">=4.21.6" -pyunormalize = ">=15.0.0" +eth-typing = ">=5.0.0" +eth-utils = ">=5.3.0" +hexbytes = ">=2" +pydantic = ">=2.4" +pyunormalize = ">=15" pywin32 = {version = ">=223", markers = "platform_system == \"Windows\""} -requests = ">=2.16.0" -typing-extensions = ">=4.0.1" -websockets = ">=10.0.0,<14.0.0" +requests = ">=2.23.0" +websockets = ">=14" [package.extras] -dev = ["build (>=0.9.0)", "bumpversion", "eth-tester[py-evm] (>=0.11.0b1,<0.12.0b1) ; python_version > \"3.7\"", "eth-tester[py-evm] (>=0.9.0b1,<0.10.0b1) ; python_version <= \"3.7\"", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "importlib-metadata (<5.0) ; python_version < \"3.8\"", "ipfshttpclient (==0.8.0a2)", "pre-commit (>=2.21.0)", "py-geth (>=3.14.0,<4)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.21.2,<0.23)", "pytest-mock (>=1.10)", "pytest-watch (>=4.2)", "pytest-xdist (>=1.29)", "setuptools (>=38.6.0)", "sphinx (>=5.3.0)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=3.18.0)", "tqdm (>4.32)", "twine (>=1.13)", "when-changed (>=0.3.0)"] -docs = ["sphinx (>=5.3.0)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=21,<22)"] -ipfs = ["ipfshttpclient (==0.8.0a2)"] -tester = ["eth-tester[py-evm] (>=0.11.0b1,<0.12.0b1) ; python_version > \"3.7\"", "eth-tester[py-evm] (>=0.9.0b1,<0.10.0b1) ; python_version <= \"3.7\"", "py-geth (>=3.14.0,<4)"] +tester = ["eth-tester[py-evm] (>=0.14.0b1,<0.15.0b1)"] [[package]] name = "websockets" -version = "10.4" +version = "17.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false -python-versions = ">=3.7" +python-versions = ">=3.11" groups = ["main"] files = [ - {file = "websockets-10.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48"}, - {file = "websockets-10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab"}, - {file = "websockets-10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c"}, - {file = "websockets-10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032"}, - {file = "websockets-10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4"}, - {file = "websockets-10.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50"}, - {file = "websockets-10.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8"}, - {file = "websockets-10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1"}, - {file = "websockets-10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331"}, - {file = "websockets-10.4-cp310-cp310-win32.whl", hash = "sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a"}, - {file = "websockets-10.4-cp310-cp310-win_amd64.whl", hash = "sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089"}, - {file = "websockets-10.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4"}, - {file = "websockets-10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f"}, - {file = "websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c"}, - {file = "websockets-10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46"}, - {file = "websockets-10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96"}, - {file = "websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa"}, - {file = "websockets-10.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c"}, - {file = "websockets-10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106"}, - {file = "websockets-10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9"}, - {file = "websockets-10.4-cp311-cp311-win32.whl", hash = "sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8"}, - {file = "websockets-10.4-cp311-cp311-win_amd64.whl", hash = "sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882"}, - {file = "websockets-10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb"}, - {file = "websockets-10.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc"}, - {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033"}, - {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41"}, - {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df"}, - {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea"}, - {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c"}, - {file = "websockets-10.4-cp37-cp37m-win32.whl", hash = "sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038"}, - {file = "websockets-10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28"}, - {file = "websockets-10.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94"}, - {file = "websockets-10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63"}, - {file = "websockets-10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf"}, - {file = "websockets-10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6"}, - {file = "websockets-10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8"}, - {file = "websockets-10.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b"}, - {file = "websockets-10.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c"}, - {file = "websockets-10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b"}, - {file = "websockets-10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56"}, - {file = "websockets-10.4-cp38-cp38-win32.whl", hash = "sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a"}, - {file = "websockets-10.4-cp38-cp38-win_amd64.whl", hash = "sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6"}, - {file = "websockets-10.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f"}, - {file = "websockets-10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112"}, - {file = "websockets-10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f"}, - {file = "websockets-10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d"}, - {file = "websockets-10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4"}, - {file = "websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0"}, - {file = "websockets-10.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c"}, - {file = "websockets-10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269"}, - {file = "websockets-10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b"}, - {file = "websockets-10.4-cp39-cp39-win32.whl", hash = "sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588"}, - {file = "websockets-10.4-cp39-cp39-win_amd64.whl", hash = "sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74"}, - {file = "websockets-10.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193"}, - {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342"}, - {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179"}, - {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485"}, - {file = "websockets-10.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631"}, - {file = "websockets-10.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0"}, - {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393"}, - {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576"}, - {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f"}, - {file = "websockets-10.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1"}, - {file = "websockets-10.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4"}, - {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf"}, - {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3"}, - {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13"}, - {file = "websockets-10.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72"}, - {file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"}, + {file = "websockets-17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:88b882764ef65147a7a5ae13168dedbe225a04e2ff4858fe543f2c402f093e9c"}, + {file = "websockets-17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98a5b2589a56a4b4f098b0a958099a4356ab904a7844f1da3841efca469af7e9"}, + {file = "websockets-17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:020e271205f8ab3406d7a59cd00de6dec722315924411c421bd00642f18bad86"}, + {file = "websockets-17.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:65be6bda2b537fefa4b3a5ccd6ab386533ce39dd8fe62433ec90901fdc81752d"}, + {file = "websockets-17.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c84bdef916556cbe1d5a43b423398be4dd3cba6522b463e53d848578b920695"}, + {file = "websockets-17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47a62d6045c6eaa0d8f97bc2fb68b8cf90077a0cbfd4e83d6f2d2145611ee134"}, + {file = "websockets-17.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34879e19bb0a3c44f9317679435aea5327fac993933a704cbf353bf1234b10c7"}, + {file = "websockets-17.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d72879819f5145a342d0030c418702496c65a4b913ef81f5ae944dd91dd50f6"}, + {file = "websockets-17.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f25e099fdfe3b09f953d84698f729a1f7d1e99101b2787d7a28ed77b323750"}, + {file = "websockets-17.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:469355ab1af100b9380f1afb09985019f4a4b94fa1dd0e9396db4361626d7ab8"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00679b7468b4c2b12b0757118174e8eabac56bb2f579a928a104d9554a56e098"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a9fe648abd1d9b89aebfa30407bfdd08a0271ec5dc7d44a4c6ccd1ce22cf562a"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f47aafd92aa28b941180e6da8a42b0f711851b14b81a5b6bb28dbbb1fa35152c"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c89406fa3dcd4aa8662c6406cc5c0de1790e9614d2c3aaf03ca53a8a8ccf3405"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b3b451fd2723ad3191a209afe6f3f4bc86c83e9a85bdc255353b91803ee6aa66"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:054c28db2dcec0e857e3b705d8c28012613e555b38c765d6a4f75340a4fc06a0"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8e822efd54137d8cc8310eb64635ab827a4a6c72ff08691f38aa624776d8ecb"}, + {file = "websockets-17.1-cp311-cp311-win32.whl", hash = "sha256:dcb8d5f7edef7a399d322cf28d4c4e6f98dab64d301c8f50581a1080e5198142"}, + {file = "websockets-17.1-cp311-cp311-win_amd64.whl", hash = "sha256:b1bc819c6db90e8f91a38250a1ab4c058261871aa52d2fe36382eddedf146dee"}, + {file = "websockets-17.1-cp311-cp311-win_arm64.whl", hash = "sha256:edadce7a22052056fd4384543019856b34850363c9d387929f677ae01d79709c"}, + {file = "websockets-17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:76dd004f59115087c7b700474cb18f01325e37250032e19396c08ae41448e4b3"}, + {file = "websockets-17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:581fa678ef46f4277cc8491312468e582f8ad609dbab907ba6096a08c6a0ff98"}, + {file = "websockets-17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87f0d5e77548b0c40c8464cdb6108792e7e53f487c6400028a4ec28a8afbe5ab"}, + {file = "websockets-17.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:882af300d2c6a092b93767d5de03c7bb56dfb06314140c8e872d3f48e09f7b74"}, + {file = "websockets-17.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c863507ada5805517ca6dff1c524dcd42942efe6304dacf06700878398d21a6"}, + {file = "websockets-17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d41ef69d5416fbc1d98cf96c37be6192d10fd101c3e0f8b3ddc36e09432b3c08"}, + {file = "websockets-17.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5aefe78e6a3077fe22b5e64b04666a85a3eb8b934d40e8595a693adcbceb6f11"}, + {file = "websockets-17.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f64e001bb7fa89b9f32cfa600bf8e9ac8ca26759d9b92ae01453ee303d9cd7b4"}, + {file = "websockets-17.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:677014a073bcb1fbaa7e21144786864f16c08f856d66834f611eceb9006cbab8"}, + {file = "websockets-17.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0de501b7f2db11e83739ac20e2d33d46da4604b829f506c24be80e7def069391"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f62114a54117e4948a1e414e89521f7fe1e3c2f83f2a571a06a4fc6718b0900a"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eec113a5b41d124ef42ff56b0d74a6da3fd986400038eab9e58ee42a4024e837"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5f051f8030a51815dc00e24bd2e5f1435af095c1cc111d747ac6e2a3620d7641"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:655a8e28010f09fd6fa317e857afab3af7647f33e41dee88fa421e92086d1090"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dc2b79afc074d2f3e64b26539350f697fe1b85ea1c49ea24eb588f247b053ce1"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e4bd7eacb87d8cf3ed70d6392c770a0d92441f05d7d2a3efafb5bc171d5e3067"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ccbf3f4a9890d50b3a08ee04029fde30a03bfdeffaa19977628bf17251764e60"}, + {file = "websockets-17.1-cp312-cp312-win32.whl", hash = "sha256:7e724f843fa6a0614aece65a7c73e51d0f4412ca41dccac13c3caf98e69536bb"}, + {file = "websockets-17.1-cp312-cp312-win_amd64.whl", hash = "sha256:617243e19a0992095956f406ee9cd3bc4ba92862d83cb1d83bb59ce574412bec"}, + {file = "websockets-17.1-cp312-cp312-win_arm64.whl", hash = "sha256:9f4a08ff7cb68c27b18e09223cc6304e01d0f82d5a240d251266dfd2e6e44729"}, + {file = "websockets-17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e"}, + {file = "websockets-17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc"}, + {file = "websockets-17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80"}, + {file = "websockets-17.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce"}, + {file = "websockets-17.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51"}, + {file = "websockets-17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c"}, + {file = "websockets-17.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3"}, + {file = "websockets-17.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435"}, + {file = "websockets-17.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906"}, + {file = "websockets-17.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d"}, + {file = "websockets-17.1-cp313-cp313-win32.whl", hash = "sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608"}, + {file = "websockets-17.1-cp313-cp313-win_amd64.whl", hash = "sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f"}, + {file = "websockets-17.1-cp313-cp313-win_arm64.whl", hash = "sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2"}, + {file = "websockets-17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0c9982938980e086da59f70d05f9418cd143401a601a0faac10fa48f7bb1cd3e"}, + {file = "websockets-17.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:57b39dc8541cf7ed3f639da82bf7451060483967f9e733da1f8173e4095f0642"}, + {file = "websockets-17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:96abdecbaae746851b87c3a36cb4a661df93ca3d92f114270f79228bf1d00de6"}, + {file = "websockets-17.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9fc873e239c5abeb150bc24dbd1a7af23a9254526383ce0a077f5e20adbeb19"}, + {file = "websockets-17.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f42912fa9eb4cb7c7ec9fde9b3332ba339eb8a8811981043d4029599f3d950b"}, + {file = "websockets-17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98bf378d7a5be047a044a1a27c987a8f355e10e3b5754617dbe756248cbc5ce"}, + {file = "websockets-17.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d334d11398086bb5559606cb42d51c013ea7c061c7db701521392373d3c087f5"}, + {file = "websockets-17.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c27336b1a0ac56569493e858497870347854372395f50483725f8cdacc5a45c"}, + {file = "websockets-17.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67258b00302a5aaf0b267771c7014b13429abd7ea17eebc4c55bd935ff101555"}, + {file = "websockets-17.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:455ffeea0879d313205df1e745e5883e1feb7f31ecd26be882f5f0babd3db04f"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7233eaf441a345a5943a929fd4b5ea3278f11aed35a9ed0f3106b8cb3ca846a"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c65da239a5ad553619804c1f9d65c1a0b3005381c6158ee14da2c7444cbd0c78"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fa1ffa08c81a4f809cdab6129f8e55bee4650b9d6d3461019dda73aacd146b6"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:406b8107943a43ef4649b1e0cb0cdc052bbf08fe1c8905a623c4af9586e5cebb"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4e8ffcb486c8490a34a4cef5e4409d8da5a1cb1681e5bf7d786ce5e84aa8540d"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fb88076df585b69c5761c387c0081aa87d7b9eb1b205a6535ca4777e25650d81"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5d4724255fb8398acd9e583b97eb2279cec20e0bd0f9a94bf75f6056ef9f13da"}, + {file = "websockets-17.1-cp314-cp314-win32.whl", hash = "sha256:be3f0129c5654517b2abf07dcb75bb1d9479759a4ccfb569e8293579e9fc029a"}, + {file = "websockets-17.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a4dc6ef83f4559e0d05f313a375cb38f63c986096a9da99fe94fdd779d313e5"}, + {file = "websockets-17.1-cp314-cp314-win_arm64.whl", hash = "sha256:46c0331c9eaaf73a559f3a9e388466be0df96eb83d40f06f1ca6ab6613b35c82"}, + {file = "websockets-17.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d411ea5ca18ac1b12c0c94be88b60c18ca641ac43bcdfdf1c9f79d46cdbe1603"}, + {file = "websockets-17.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:07fa3e7c30e2c577928d359b56bf872a3e0cbcc15553eaa0907c1ee86344b56f"}, + {file = "websockets-17.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de9acef07e3a78e9567fcd26c29011a4da8f050b13004bbf880a0fd82a6eea5"}, + {file = "websockets-17.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ea0ed9373b880115911d9d39634bccc95b8ce590c9c42e8589f5cacc3ef3cee2"}, + {file = "websockets-17.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50903d335bfda026c2fa11dd9aed09d8cbee0c451e3a85122a9acb041b7dc69b"}, + {file = "websockets-17.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a74531ce81af587f906ab42f194032388fcff8fc7938402e5917c9147a39441"}, + {file = "websockets-17.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8fbf28e639544503b7d1c96452a5e5e043e4108d89b1f3fa02910603622d19db"}, + {file = "websockets-17.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f612dc57f00c07cf4aa2673f7cbceabd654ad2457b7e639f061b794d6e11f9fd"}, + {file = "websockets-17.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c7ac77401227212dc6e849182feee50d57cf456ec6329ffd6979c94bb136c5c"}, + {file = "websockets-17.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32a2a68d989d6e5b74a9d5095415c51189ebae29fceb7cf2b64a1c0318a81256"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aec00f018d34c67500ff0438dc314b40277be4a1b983cbacbf53ccf7db63e257"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0014eaff8ad5b3b43feda2279f9d34bf2eaae040720b9fbbb55944b10f40b14d"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:db9d7ee47f3ba531e278be539af39e2c7c7d28fb94897b6cd1120d63b0ef5922"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ff3e2ba7a9f0a110b0555452e9b5a03a34e11662544e01beea15f144b48ba7b7"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6da17fc94bd270f5987b10bee113461ac36a36a98b0481ddcc98056e5a90001a"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e8dc3fa6d6b7ead3f9de57895f41b116a28787548e066365d9d90f7356bcaad2"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b65d5fe48219dc2d5e158de9e6514e75600f379cc7e37108d35f31764c155566"}, + {file = "websockets-17.1-cp314-cp314t-win32.whl", hash = "sha256:2cce251f3e2469b99b6802b55435bcdd07123b41870f54c87b336183af9d7e68"}, + {file = "websockets-17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f6c38cdcaf98a911d7acc25577f2f9e710f3a2fc2bde1563556784320196b51"}, + {file = "websockets-17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d1e2f5fa2b6d01f0d85b4f223fea7ed1d504be282a02a81bd2be4817ef7a2f03"}, + {file = "websockets-17.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:88381602e379165b66244b2ebc29f9b23ea0851fbe63ae157f91ca324f072d6f"}, + {file = "websockets-17.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:88bc5138e53903a85c354e59df7ba73ce306f7b09724cef74dba121e60a88ce2"}, + {file = "websockets-17.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3546ef55b3a074494106508bc6505c73825970d2d9505f7bf53882b3e88b0d1e"}, + {file = "websockets-17.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9ae55d24241fc055f22aea3ac924559069848bd0ad4ea065fdd72d2194685fe8"}, + {file = "websockets-17.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7b349265fad6244013eecd99df8d83c12bf3013943e431f4fadd5bffc37db42"}, + {file = "websockets-17.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc5789e5ea182b77a38881383ada5347202a6c66f4857d054e075290e80b604b"}, + {file = "websockets-17.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce13c7d233239e739600a57d4a73c1192ad8259e655a4d55aa1a454242bc809d"}, + {file = "websockets-17.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1036189bd34b0bc1b10a4679321e2c7968af317efe6e8e4c1c5141c4254fb5bb"}, + {file = "websockets-17.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e78fd4b7b2c5086a38671c9c882c1e643385eccea360b5b1fda4a105e590087e"}, + {file = "websockets-17.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46e7a10bf04318c7b0c0273791925ae5e1cbe4a11e34aa934d2ef27862058a80"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:33e45c7ea38428e740a7f233555d71df0b875cef7fc080acebc9654475e35335"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6e63c01803be425ff062b7f7fc201a74def1d49fc94a2410dd17375df75936e9"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:722ec21717eec6477bce582147a28acdfe034e604239466a6a95daedb863e774"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:e74e41f0ad12ff1e8983e349daef79d37cc8280c743ce9d134d6c74c18dab5d6"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:12fe8984a32dbfd084e0603f1a8d740c0180cb85b3174585c54a80d2455a8394"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:01dcb47deebc40b38fd4a493b9b9f4d0a704b7bec6f35e4d34085b329abce71a"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f4c45ee2512d3757b5e6c67c5a34e435143f2ecb7df3324f9fd888688c45c0f4"}, + {file = "websockets-17.1-cp315-cp315-win32.whl", hash = "sha256:0f4f50dfe2cc810fc4e2de979b35e83bf8bb4bccdc6fe472d93762ea7b1d5927"}, + {file = "websockets-17.1-cp315-cp315-win_amd64.whl", hash = "sha256:4af784f3e436f65b355c117c6497320f2b5cf6a559295cb1c4c7338e335d45cc"}, + {file = "websockets-17.1-cp315-cp315-win_arm64.whl", hash = "sha256:d58159af7835fde09c462394293c0d7aaf8fb4557d8f8e5699f5e722ccae013d"}, + {file = "websockets-17.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1a5cf4e7bbe3ca499e6a289206cb4fcb7444b09919e129bd517f57d5fa192c13"}, + {file = "websockets-17.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:416b4bc8789a1865a3ff643ec4ee073a5f52402d0dbeafd27b1798d5dd6b6a51"}, + {file = "websockets-17.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:259f45358c76d3b18489e3e80636cdbe807e05ecf1b10fdf1a779106d23d0c8e"}, + {file = "websockets-17.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9d01e8ede41fea4f5a847dad9d628355f74905f437a5b6856d67aa66d193800"}, + {file = "websockets-17.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b35181a14cbfcae163b4de545d22abfd07d06c2c41ca69cfcd99251d6888ab"}, + {file = "websockets-17.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a8e768a048c2220697477ce2e67e4345dc9f693d0ee6af53945b5e30227c6a7"}, + {file = "websockets-17.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:880069d21cc33a558dcf180924a546d1ecf8ada5be3e4e70acee87019d706a24"}, + {file = "websockets-17.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cec1bb8f22abccc8d20f8ca63df9be41600c26c190f4b97ee86c675fd4a863a6"}, + {file = "websockets-17.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3a1d577e081667dda7f8e5b4796e6e32f9713c93e2a3d930669519840a3c623"}, + {file = "websockets-17.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc053f9e95a76213c5eb7ed95779f7daf0d2bf0e4e03073629ebfa43a033f151"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bb0efe019480a1c93e168ce96479273aaebd672fc8c350d5eed1e507ababb1b8"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:615746b12b26a3fd4077bc6fbeb277a1c192a45dd57b531d07ad9ed5c52a9a7a"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1a20136d61f9ca3a31493732762661fafc2c20e8861930214e21afc6a8a692a2"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:2786cbd273ab69c22612db8a41229ddf2c158060b17b5928884bf388d07887f3"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c323fc3be1dc3f87f6c59458cb7d9e13dcbbf971d6c3f3e2bbaf58d3bfcdfe"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12c8e2b25df59755954a04dfa09c990b96691025aaf7eafd19ed6da24b09c18d"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f58f58b4b29bbea2a3635e2c56eff4a3adab011fe383802a9e542e31b97085fc"}, + {file = "websockets-17.1-cp315-cp315t-win32.whl", hash = "sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3"}, + {file = "websockets-17.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110"}, + {file = "websockets-17.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81"}, + {file = "websockets-17.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10ecb38ffc05e1841b619d99c725307a223ef9ad58e7b1ed33311d472dc43918"}, + {file = "websockets-17.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17aa424ab61620aad21b36b2240efc87b500cc496e7d0e999a5c2ae99395e886"}, + {file = "websockets-17.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:764cf7bfa149365f32b7a0fd9fed32debdac29dd06295d5635cde1745b446cd8"}, + {file = "websockets-17.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d1b108bd8f5f6a8b90801f6db3b3858d5deca889acfdb8ac497bbb24e4b0edf"}, + {file = "websockets-17.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a62d8c424383c9dc769ff3672018df822603117e32686e567d452ed035b6fb2e"}, + {file = "websockets-17.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8196d217eeca52b9235ee1f8a684a09885a5f953d5a31e80ef915bf2c5c94f9d"}, + {file = "websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23"}, + {file = "websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a"}, ] [[package]] name = "yarl" -version = "1.8.2" +version = "1.24.5" description = "Yet another URL library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"}, - {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"}, - {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8"}, - {file = "yarl-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3"}, - {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80"}, - {file = "yarl-1.8.2-cp310-cp310-win32.whl", hash = "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42"}, - {file = "yarl-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574"}, - {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634"}, - {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd"}, - {file = "yarl-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76"}, - {file = "yarl-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c"}, - {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2"}, - {file = "yarl-1.8.2-cp311-cp311-win32.whl", hash = "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b"}, - {file = "yarl-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c"}, - {file = "yarl-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89"}, - {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7"}, - {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37"}, - {file = "yarl-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89"}, - {file = "yarl-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5"}, - {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1"}, - {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918"}, - {file = "yarl-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3"}, - {file = "yarl-1.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c"}, - {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946"}, - {file = "yarl-1.8.2-cp38-cp38-win32.whl", hash = "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165"}, - {file = "yarl-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f"}, - {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8"}, - {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf"}, - {file = "yarl-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08"}, - {file = "yarl-1.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516"}, - {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588"}, - {file = "yarl-1.8.2-cp39-cp39-win32.whl", hash = "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83"}, - {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"}, - {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, + {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, + {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, + {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, + {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, + {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, + {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, + {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, + {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, + {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, + {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, + {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, + {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, + {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, + {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} - -[[package]] -name = "zipp" -version = "3.15.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, -] -markers = {main = "python_version < \"3.9\"", dev = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8 ; python_version < \"3.12\"", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\""] +propcache = ">=0.2.1" [metadata] lock-version = "2.1" -python-versions = "^3.7.2" -content-hash = "d1eab9ee3f4a8dbb34d648c49c215322cd186bd6d680865ae7dbd87394d6f6ba" +python-versions = "^3.12" +content-hash = "b04ace60c79942d0057ba54af1f49a8078da05696a30caf80353269a980b84c0" diff --git a/pyproject.toml b/pyproject.toml index 24cc5d0c..a08b4f0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "uniswap-python" -version = "0.8.0" # this is automatically set in CI on tagged releases (before pushed to PyPI) +version = "0.9.0" # this is automatically set in CI on tagged releases (before pushed to PyPI) description = "An unofficial Python wrapper for the decentralized exchange Uniswap" repository = "https://github.com/shanefontaine/uniswap-python" readme = "README.md" @@ -23,32 +23,42 @@ classifiers = [ unipy = "uniswap:main" [tool.poetry.dependencies] -python = "^3.7.2" -web3 = { version = "^6.5.0", allow-prereleases = true } -click = "^8.0.3" +python = "^3.12" +web3 = "^8.0.0" +cytoolz = ">=0.12.3" +frozenlist = ">=1.5.0" +multidict = ">=6.0.5" +yarl = ">=1.10.0" +click = "^8" python-dotenv = "*" -typing-extensions = "*" +aiohttp = ">=3.9.0" +typing-extensions = ">=4.12.0" +# eth-keyfile (via web3->eth-account) imports pkg_resources, which setuptools +# dropped in 82.0.0 (last shipped in 81.0.0). Pin below that until web3 7.x +# (which pulls in a pkg_resources-free eth-keyfile) is adopted. +setuptools = "<82" [tool.poetry.dev-dependencies] mypy = "*" -black = "*" pre-commit = "*" -pytest = "^6.0" +pytest = "^8.4" pytest-cov = "*" pytest-dotenv = "*" -flake8 = "*" -Sphinx = "*" -sphinx-book-theme = "*" +pytest-asyncio = "^1.4" + +ruff = ">=0.9" +Sphinx = "^9" +sphinx-book-theme = "^1.1" sphinx-click = "*" -pydata-sphinx-theme = "0.13.1" # due to: https://github.com/executablebooks/sphinx-book-theme/issues/711 +pydata-sphinx-theme = "*" [tool.pytest.ini_options] log_cli = false # to print logs during tests, set to true #log_level = "NOTSET" -[tool.ruff] +[tool.ruff.lint] ignore = ["E402", "E501"] [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core>=1.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/test_asyncuniswap.py b/tests/test_asyncuniswap.py new file mode 100644 index 00000000..6e7c1e61 --- /dev/null +++ b/tests/test_asyncuniswap.py @@ -0,0 +1,651 @@ +import logging +import os +import shutil +import subprocess +from collections.abc import Generator +from contextlib import contextmanager +from dataclasses import dataclass +from time import sleep + +import pytest +import pytest_asyncio +from web3 import AsyncWeb3 +from web3.types import Wei + +from uniswap import AsyncUniswap +from uniswap.constants import ETH_ADDRESS +from uniswap.exceptions import InvalidFeeTier +from uniswap.fee import FeeTier +from uniswap.tokens import get_tokens +from uniswap.util import ( + _addr_to_str, + _str_to_addr, + default_tick_range, +) + +pytestmark = pytest.mark.skipif( + os.getenv("UNISWAP_VERSION") == "4", + reason="This test file is for Uniswap v1, v2, and v3. For Uniswap v4 tests, see test_uniswap4.py", +) + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +ENV_UNISWAP_VERSION = os.getenv("UNISWAP_VERSION", None) +if ENV_UNISWAP_VERSION: + UNISWAP_VERSIONS = [int(ENV_UNISWAP_VERSION)] +else: + UNISWAP_VERSIONS = [1, 2, 3] + +RECEIPT_TIMEOUT = 5 + + +ONE_ETH = 10**18 +ONE_DAI = 10**18 +ONE_USDC = 10**6 + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + +@dataclass +class AnvilInstance: + provider: str + eth_address: str + eth_privkey: str + + +@pytest.fixture(scope="module", params=UNISWAP_VERSIONS) +def client(request, web3: AsyncWeb3, anvil: AnvilInstance): + return AsyncUniswap( + anvil.eth_address, + anvil.eth_privkey, + web3=web3, + version=request.param, + use_estimate_gas=False, # see note in _build_and_send_tx + ) + + +@pytest.fixture(scope="function") +def tokens(client: AsyncUniswap): + return get_tokens(client.netname) + + +@pytest.fixture(scope="module") +@pytest.mark.asyncio +async def test_assets(client: AsyncUniswap): + """ + Buy some DAI and USDC to test with. + """ + tokens = get_tokens(client.netname) + + for token_name, amount in [ + ("DAI", 10_000 * ONE_DAI), + ("USDC", 10_000 * ONE_USDC), + ]: + token_addr = tokens[token_name] + price = await client.get_price_output( + _str_to_addr(ETH_ADDRESS), token_addr, amount, fee=FeeTier.TIER_3000 + ) + logger.info(f"Cost of {amount} {token_name}: {price}") + logger.info("Buying...") + + txid = await client.make_trade_output( + tokens["ETH"], token_addr, amount, fee=FeeTier.TIER_3000 + ) + tx = await client.w3.eth.wait_for_transaction_receipt( + txid, timeout=RECEIPT_TIMEOUT + ) + assert tx["status"] == 1, f"Transaction failed: {tx}" + + +@pytest_asyncio.fixture(scope="module") +async def web3(anvil: AnvilInstance): + w3: AsyncWeb3 = AsyncWeb3( + AsyncWeb3.AsyncHTTPProvider(anvil.provider, request_kwargs={"timeout": 30}) + ) + if 1 != int(await w3.net.version): + logger.warning("PROVIDER was not a mainnet provider, which the tests require") + return w3 + + +@pytest.fixture(scope="module") +def anvil() -> Generator[AnvilInstance, None, None]: + """Fixture that runs anvil which has forked off mainnet""" + if not shutil.which("anvil"): + raise ValueError( + "anvil was not found in PATH, install Foundry: https://getfoundry.sh" + ) + if "PROVIDER" not in os.environ: + raise ValueError( + "PROVIDER was not set, you need to set it to a mainnet provider (such as Infura) so that we can fork off our testnet" + ) + + port = 11999 + defaultGasPrice = 100_000_000_000 # 100 gwei + p = subprocess.Popen( + f"""anvil + --port {port} + --chain-id 1 + --fork-url {os.environ["PROVIDER"]} + --gas-price {defaultGasPrice} + """.replace("\n", " "), + shell=True, + ) + # Account #9 from anvil's default test mnemonic, starts with 1000 ETH + eth_address = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" + eth_privkey = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" + + sleep(3) + yield AnvilInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) + p.kill() + p.wait() + + +@contextmanager +def does_not_raise(): + yield + + +ONE_ETH = 10**18 +ONE_USDC = 10**6 + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + +@pytest.mark.usefixtures("client", "web3") +class TestAsyncUniswap: + # ------ Exchange ------------------------------------------------------------------ + @pytest.mark.asyncio + async def test_get_fee_maker(self, client: AsyncUniswap): + if client.version not in [1, 2]: + pytest.skip("Tested method not supported in this Uniswap version") + r = client.get_fee_maker() + assert r == 0 + + @pytest.mark.asyncio + async def test_get_fee_taker(self, client: AsyncUniswap): + if client.version not in [1, 2]: + pytest.skip("Tested method not supported in this Uniswap version") + r = client.get_fee_taker() + assert r == 0.003 + + # ------ Market -------------------------------------------------------------------- + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0", + [ + ("UNI"), + ("DAI"), + ("USDC"), + ], + ) + async def test_approve(self, client: AsyncUniswap, tokens, token0): + token0 = tokens[token0] + if client.version == 1 and ETH_ADDRESS == token0: + pytest.skip("Not supported in this version of Uniswap") + r = await client.approve(token0) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, qty", + [ + ("ETH", "UNI", ONE_ETH), + ("UNI", "ETH", ONE_ETH), + ("ETH", "DAI", ONE_ETH), + ("DAI", "ETH", ONE_ETH), + ("ETH", "UNI", 2 * ONE_ETH), + ("UNI", "ETH", 2 * ONE_ETH), + ("WETH", "DAI", ONE_ETH), + ("DAI", "WETH", ONE_ETH), + ("DAI", "USDC", ONE_ETH), + ], + ) + async def test_get_price_input( + self, client: AsyncUniswap, tokens, token0, token1, qty + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version == 1 and ETH_ADDRESS not in [token0, token1]: + pytest.skip("Not supported in this version of Uniswap") + r = await client.get_price_input(token0, token1, qty, fee=FeeTier.TIER_3000) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, qty", + [ + ("ETH", "UNI", ONE_ETH), + ("UNI", "ETH", ONE_ETH // 100), + ("ETH", "DAI", ONE_ETH), + ("DAI", "ETH", ONE_ETH), + ("ETH", "UNI", 2 * ONE_ETH), + ("WETH", "DAI", ONE_ETH), + ("DAI", "WETH", ONE_ETH), + ("DAI", "USDC", ONE_USDC), + ], + ) + async def test_get_price_output( + self, client: AsyncUniswap, tokens, token0, token1, qty + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version == 1 and ETH_ADDRESS not in [token0, token1]: + pytest.skip("Not supported in this version of Uniswap") + r = await client.get_price_output(token0, token1, qty, fee=FeeTier.TIER_3000) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, fee", [("DAI", "USDC", FeeTier.TIER_3000)] + ) + async def test_get_raw_price( + self, client: AsyncUniswap, tokens, token0, token1, fee + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version == 1: + pytest.skip("Only supported on Uniswap v2 and v3") + r = await client.get_raw_price(token0, token1, fee=fee) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, kwargs", + [ + ("WETH", "DAI", {"fee": FeeTier.TIER_3000}), + ], + ) + async def test_get_pool_instance( + self, client: AsyncUniswap, tokens, token0, token1, kwargs + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + r = await client.get_pool_instance(token0, token1, **kwargs) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, kwargs", + [ + ("WETH", "DAI", {"fee": FeeTier.TIER_3000}), + ], + ) + async def test_get_pool_immutables( + self, client: AsyncUniswap, tokens, token0, token1, kwargs + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + pool = await client.get_pool_instance(token0, token1, **kwargs) + r = await client.get_pool_immutables(pool) + print(r) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, kwargs", + [ + ("WETH", "DAI", {"fee": FeeTier.TIER_3000}), + ], + ) + async def test_get_pool_state( + self, client: AsyncUniswap, tokens, token0, token1, kwargs + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + pool = await client.get_pool_instance(token0, token1, **kwargs) + r = await client.get_pool_state(pool) + print(r) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "amount0, amount1, token0, token1, kwargs", + [ + (1, 10, "WETH", "DAI", {"fee": FeeTier.TIER_3000}), + ], + ) + async def test_mint_position( + self, client: AsyncUniswap, tokens, amount0, amount1, token0, token1, kwargs + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + pool = await client.get_pool_instance(token0, token1, **kwargs) + r = await client.mint_position(pool, amount0, amount1) + print(r) + assert r + + # ------ ERC20 Pool ---------------------------------------------------------------- + @pytest.mark.asyncio + @pytest.mark.parametrize("token", [("UNI"), ("DAI")]) + async def test_get_ex_eth_balance( + self, + client: AsyncUniswap, + tokens, + token, + ): + if client.version != 1: + pytest.skip("Only supported on Uniswap v1") + r = await client.get_ex_eth_balance(tokens[token]) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize("token", [("UNI"), ("DAI")]) + async def test_get_ex_token_balance( + self, + client: AsyncUniswap, + tokens, + token, + ): + if client.version != 1: + pytest.skip("Only supported on Uniswap v1") + r = await client.get_ex_token_balance(tokens[token]) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize("token", [("UNI"), ("DAI")]) + async def test_get_exchange_rate( + self, + client: AsyncUniswap, + tokens, + token, + ): + if client.version != 1: + pytest.skip("Only supported on Uniswap v1") + r = await client.get_exchange_rate(tokens[token]) + assert r + + # ------ Liquidity ----------------------------------------------------------------- + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, amount0, amount1, qty, fee", + [ + ("DAI", "USDC", ONE_ETH, ONE_USDC, ONE_ETH, FeeTier.TIER_3000), + ], + ) + async def test_v3_deploy_pool_with_liquidity( + self, client: AsyncUniswap, tokens, token0, token1, amount0, amount1, qty, fee + ): + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + + try: + pool = await client.create_pool_instance( + tokens[token0], tokens[token1], fee + ) + except Exception: + pool = await client.get_pool_instance(tokens[token0], tokens[token1], fee) + + print(pool.address) + # Ensuring client has sufficient balance of both tokens + eth_to_dai = await client.make_trade( + tokens["ETH"], + tokens[token0], + qty, + client.address, + fee=fee, + ) + eth_to_dai_tx = await client.w3.eth.wait_for_transaction_receipt( + eth_to_dai, timeout=RECEIPT_TIMEOUT + ) + assert eth_to_dai_tx["status"] + dai_to_usdc = await client.make_trade( + tokens[token0], + tokens[token1], + qty * 10, + client.address, + fee=fee, + ) + dai_to_usdc_tx = await client.w3.eth.wait_for_transaction_receipt( + dai_to_usdc, timeout=RECEIPT_TIMEOUT + ) + assert dai_to_usdc_tx["status"] + + balance_0 = await client.get_token_balance(tokens[token0]) + balance_1 = await client.get_token_balance(tokens[token1]) + + assert balance_0 > amount0, f"Have: {balance_0} need {amount0}" + assert balance_1 > amount1, f"Have: {balance_1} need {amount1}" + + min_tick, max_tick = default_tick_range(fee) + r = await client.mint_liquidity( + pool, + amount0, + amount1, + tick_lower=min_tick, + tick_upper=max_tick, + deadline=2**64, + ) + assert r["status"] + + position_balance = await client.nonFungiblePositionManager.functions.balanceOf( + _addr_to_str(client.address) + ).call() + assert position_balance > 0 + + position_array = await client.get_liquidity_positions() + assert len(position_array) > 0 + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "deadline", + [(2**64)], + ) + async def test_close_position(self, client: AsyncUniswap, deadline): + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + position_array = await client.get_liquidity_positions() + tokenId = position_array[0] + r = await client.close_position(tokenId, deadline=deadline) + assert r["status"] + + @pytest.mark.asyncio + @pytest.mark.parametrize("token0, token1", [("DAI", "USDC")]) + async def test_get_tvl_in_pool_on_chain( + self, client: AsyncUniswap, tokens, token0, token1 + ): + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + + pool = await client.get_pool_instance( + tokens[token0], tokens[token1], fee=FeeTier.TIER_3000 + ) + tvl_0, tvl_1 = await client.get_tvl_in_pool(pool) + assert tvl_0 > 0 + assert tvl_1 > 0 + + @pytest.mark.asyncio + @pytest.mark.skip + @pytest.mark.parametrize( + "token, max_eth", + [ + ("UNI", 0.00001 * ONE_ETH), + ("DAI", 0.00001 * ONE_ETH), + ], + ) + async def test_add_liquidity( + self, client: AsyncUniswap, tokens, web3: AsyncWeb3, token, max_eth + ): + token = tokens[token] + r = await client.add_liquidity(token, max_eth) + tx = await web3.eth.wait_for_transaction_receipt(r, timeout=RECEIPT_TIMEOUT) + assert tx["status"] + + @pytest.mark.asyncio + @pytest.mark.skip + @pytest.mark.parametrize( + "token, max_token, expectation", + [ + ("UNI", 0.00001 * ONE_ETH, does_not_raise()), + ("DAI", 0.00001 * ONE_ETH, does_not_raise()), + ], + ) + async def test_remove_liquidity( + self, + client: AsyncUniswap, + web3: AsyncWeb3, + tokens, + token, + max_token, + expectation, + ): + token = tokens[token] + with expectation: + r = await client.remove_liquidity(tokens[token], max_token) + tx = await web3.eth.wait_for_transaction_receipt(r) + assert tx["status"] + + # ------ Make Trade ---------------------------------------------------------------- + @pytest.mark.asyncio + @pytest.mark.parametrize( + "input_token, output_token, qty, recipient, expectation", + [ + # ETH -> Token + ("ETH", "DAI", ONE_ETH, None, does_not_raise), + # Token -> Token + ("DAI", "USDC", ONE_ETH, None, does_not_raise), + # Token -> ETH + ("USDC", "ETH", ONE_USDC // 2, None, does_not_raise), + # ("ETH", "UNI", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), + # ("UNI", "ETH", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), + # ("DAI", "UNI", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), + ], + ) + async def test_make_trade( + self, + client: AsyncUniswap, + web3: AsyncWeb3, + tokens, + test_assets, + input_token, + output_token, + qty: int, + recipient, + expectation, + ): + input_token, output_token = tokens[input_token], tokens[output_token] + if client.version == 1 and ETH_ADDRESS not in [input_token, output_token]: + pytest.skip( + "Not supported in this version of Uniswap, or at least no liquidity" + ) + # Uniswap v1 token-to-ETH uses Vyper 0.1.x bytecode with non-standard JUMP + # patterns that Ganache tolerated but Anvil's strict revm rejects (InvalidJump). + # xfail until v1 is formally deprecated or a compatible fork-mode is found. + if client.version == 1 and output_token == ETH_ADDRESS: + pytest.xfail( + "v1 token-to-ETH: EvmError: InvalidJump — Vyper 0.1.x bytecode " + "incompatible with Anvil revm; tracked for v1 deprecation" + ) + with expectation(): + bal_in_before = await client.get_token_balance(input_token) + + txid = await client.make_trade( + input_token, output_token, qty, recipient, fee=FeeTier.TIER_3000 + ) + tx = await web3.eth.wait_for_transaction_receipt( + txid, timeout=RECEIPT_TIMEOUT + ) + assert tx["status"], f"Transaction failed with status {tx['status']}: {tx}" + + # TODO: Checks for ETH, taking gas into account + bal_in_after = await client.get_token_balance(input_token) + if input_token != tokens["ETH"]: + assert bal_in_before - qty == bal_in_after + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "input_token, output_token, qty, recipient, expectation", + [ + # ETH -> Token + ("ETH", "DAI", ONE_ETH, None, does_not_raise), + # Token -> Token + ("DAI", "USDC", ONE_USDC, None, does_not_raise), + # Token -> ETH + ("DAI", "ETH", ONE_ETH // 10, None, does_not_raise), + # FIXME: These should probably be uncommented eventually + # ("ETH", "UNI", int(0.000001 * ONE_ETH), ZERO_ADDRESS), + # ("UNI", "ETH", int(0.000001 * ONE_ETH), ZERO_ADDRESS), + # ("DAI", "UNI", int(0.000001 * ONE_ETH), ZERO_ADDRESS), + ("DAI", "DAI", ONE_USDC, None, lambda: pytest.raises(ValueError)), + ], + ) + async def test_make_trade_output( + self, + client: AsyncUniswap, + web3: AsyncWeb3, + tokens, + test_assets, + input_token, + output_token, + qty: int, + recipient, + expectation, + ): + input_token, output_token = tokens[input_token], tokens[output_token] + if client.version == 1 and ETH_ADDRESS not in [input_token, output_token]: + pytest.skip( + "Not supported in this version of Uniswap, or at least no liquidity" + ) + # Same Anvil revm InvalidJump for v1 token-to-ETH (see test_make_trade above). + if client.version == 1 and output_token == ETH_ADDRESS: + pytest.xfail( + "v1 token-to-ETH: EvmError: InvalidJump — Vyper 0.1.x bytecode " + "incompatible with Anvil revm; tracked for v1 deprecation" + ) + with expectation(): + balance_before = await client.get_token_balance(output_token) + + r = await client.make_trade_output( + input_token, output_token, qty, recipient, fee=FeeTier.TIER_3000 + ) + tx = await web3.eth.wait_for_transaction_receipt(r, timeout=RECEIPT_TIMEOUT) + assert tx["status"] + + # # TODO: Checks for ETH, taking gas into account + balance_after = await client.get_token_balance(output_token) + if output_token != tokens["ETH"]: + assert balance_before + qty == balance_after + + @pytest.mark.asyncio + async def test_fee_required_for_uniswap_v3( + self, + client: AsyncUniswap, + tokens, + ) -> None: + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + with pytest.raises(InvalidFeeTier): + await client.get_price_input( + tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None + ) + with pytest.raises(InvalidFeeTier): + await client.get_price_output( + tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None + ) + with pytest.raises(InvalidFeeTier): + await client._get_eth_token_output_price(tokens["UNI"], ONE_ETH, fee=None) + with pytest.raises(InvalidFeeTier): + await client._get_token_eth_output_price( + tokens["UNI"], Wei(ONE_ETH), fee=None + ) + with pytest.raises(InvalidFeeTier): + await client._get_token_token_output_price( + tokens["UNI"], tokens["ETH"], ONE_ETH, fee=None + ) + with pytest.raises(InvalidFeeTier): + await client.make_trade(tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None) + with pytest.raises(InvalidFeeTier): + await client.make_trade_output( + tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None + ) + # NOTE: (rudiemeant@gmail.com): Since in 0.7.1 we're breaking the + # backwards-compatibility with 0.7.0, we should check + # that clients now get an error when trying to call methods + # without explicitly specifying a fee tier. + with pytest.raises(InvalidFeeTier): + await client.get_pool_instance(tokens["ETH"], tokens["UNI"], fee=None) # type: ignore[arg-type] + with pytest.raises(InvalidFeeTier): + await client.create_pool_instance(tokens["ETH"], tokens["UNI"], fee=None) # type: ignore[arg-type] + with pytest.raises(InvalidFeeTier): + await client.get_raw_price(tokens["ETH"], tokens["UNI"], fee=None) diff --git a/tests/test_asyncuniswap4.py b/tests/test_asyncuniswap4.py new file mode 100644 index 00000000..59da6b64 --- /dev/null +++ b/tests/test_asyncuniswap4.py @@ -0,0 +1,1885 @@ +import logging +import os +import shutil +import subprocess +from collections.abc import Generator +from contextlib import contextmanager +from dataclasses import astuple, dataclass +from time import sleep + +import pytest +import pytest_asyncio +from web3 import AsyncWeb3 +from web3.types import Nonce + +from uniswap import AsyncUniswap4 +from uniswap.constants import ( + ETH_ADDRESS, + ZERO_HOOK, + universal_router_commands, + v4_actions, +) +from uniswap.types import AddressLike, PoolKey +from uniswap.util import AsyncV4pools, V4pools, _addr_to_str, _str_to_addr + +pytestmark = pytest.mark.skipif( + os.getenv("UNISWAP_VERSION") != "4", + reason="This test file is for Uniswap v4. For Uniswap v1, v2, and v3 tests, see test_uniswap.py", +) + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +RECEIPT_TIMEOUT = 5 + +ONE_ETH = 10**18 +ONE_USDT = 10**6 +ONE_USDC = 10**6 +ETH_DECIMALS = 18 +USDT_DECIMALS = 6 +USDC_DECIMALS = 6 +USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" +USDT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7" +ETH_USDC_FEE = 500 +ETH_USDC_TICK_SPACING = 10 +USDC_USDT_FEE = 10 +USDC_USDT_TICK_SPACING = 1 + +TOKEN_ID = 1 + +eth_usdc_poolkey: PoolKey = PoolKey( + currency0=ETH_ADDRESS, # ETH + currency1=USDC_ADDRESS, # USDC + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + hooks=ZERO_HOOK, +) + + +usdc_usdt_poolkey: PoolKey = PoolKey( + currency0=USDC_ADDRESS, # USDC + currency1=USDT_ADDRESS, # USDT + fee=USDC_USDT_FEE, + tick_spacing=USDC_USDT_TICK_SPACING, + hooks=ZERO_HOOK, +) + + +@dataclass +class AnvilInstance: + provider: str + eth_address: str + eth_privkey: str + + +@pytest.fixture(scope="module") +def client(web3: AsyncWeb3, anvil: AnvilInstance) -> AsyncUniswap4: + return AsyncUniswap4( + anvil.eth_address, + anvil.eth_privkey, + web3=web3, + gas_limit=500_000, + ) + + +@pytest.fixture(scope="module") +def pool_service(web3: AsyncWeb3) -> AsyncV4pools: + return AsyncV4pools(web3) + + +@pytest_asyncio.fixture(scope="module") +async def web3(anvil: AnvilInstance) -> AsyncWeb3: + w3 = AsyncWeb3( + AsyncWeb3.AsyncHTTPProvider(anvil.provider, request_kwargs={"timeout": 30}) + ) + if 1 != int(await w3.net.version): + logger.warning("PROVIDER was not a mainnet provider, which the tests require") + return w3 + + +@pytest.fixture(scope="module") +def anvil() -> Generator[AnvilInstance, None, None]: + """Fixture that runs anvil which has forked off mainnet""" + if not shutil.which("anvil"): + raise ValueError("anvil was not found in PATH") + if "PROVIDER" not in os.environ: + raise ValueError( + "PROVIDER was not set, you need to set it to a mainnet provider (such as Infura) so that we can fork off our testnet" + ) + + port = 11998 + defaultGasPrice = 100_000_000_000 # 100 gwei + p = subprocess.Popen( + f"""anvil + --port {port} + --chain-id 1 + --fork-url {os.environ["PROVIDER"]} + --gas-price {defaultGasPrice} + """.replace("\n", " "), + shell=True, + ) + # Address #1 when anvil is run with `--wallet.seed test`, it starts with 1000 ETH + eth_address = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" + eth_privkey = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" + sleep(3) + yield AnvilInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) + p.kill() + p.wait() + + +@contextmanager +def does_not_raise(): + yield + + +@pytest.mark.usefixtures("client", "web3") +class TestAsyncUniswap4: + test_token_id: int = 0 + test_mint_tx_hash: str = "" + test_tick: int = 0 + + # ------ Approve/tx replacement----------------------------------------------------- + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token, max_approval, delay_interval", + [ + (USDC_ADDRESS, None, 7), + (USDT_ADDRESS, 1_000_000 * ONE_USDT, 1), + ], + ) + async def test_approve( + self, + client: AsyncUniswap4, + token: str, + max_approval: int | None, + delay_interval: int, + ): + # Approve the token + tx = await client.approve( + _str_to_addr(token), + max_approval, + delay_interval=delay_interval, + approve_position_manager=True, + ) + assert tx + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + # Check that the approval was successful by calling allowance + allowance = await client.approval(_str_to_addr(token)) + assert allowance + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "address_to, gas_price, priority_fee, custom_nonce", + [ + ("self", 20, 10, None), + (ETH_ADDRESS, 20, 10, 0), + ], + ) + async def test_drop_txn( + self, + client: AsyncUniswap4, + address_to: str, + gas_price: float, + priority_fee: int, + custom_nonce: int | None, + ): + if not client.w3.is_address(address_to): + address: AddressLike = client.address + else: + address = _str_to_addr(address_to) + await client.update_last_nonce() + if custom_nonce == 0: + nonce: Nonce | None = client.last_nonce + else: + nonce = None + + tx = await client.drop_txn( + address, + gas_price, + priority_fee=priority_fee, + custom_nonce=nonce, + ) + assert tx + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + await client.update_last_nonce() + + # ------ Market/price impact-------------------------------------------------------- + # Input quotes + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_get_quote_exact_input_single( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.get_quote_exact_input_single( + token0, token1, qty, fee, tick_spacing, hooks + ) + assert result + + @pytest.mark.parametrize( + "token0, qty, route", + [ + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + ], + ), + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + ), + ], + ) + @pytest.mark.asyncio + async def test_get_quote_exact_input( + self, + client: AsyncUniswap4, + token0: str, + qty: int, + route: list[PoolKey], + ): + result = await client.get_quote_exact_input(token0, qty, route) + assert result + + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, route", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + ONE_ETH, + None, + None, + None, + None, + [eth_usdc_poolkey, usdc_usdt_poolkey], + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDT, + None, + None, + None, + None, + [usdc_usdt_poolkey, eth_usdc_poolkey], + ), + ], + ) + @pytest.mark.asyncio + async def test_get_price_input( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int | None, + tick_spacing: int | None, + hooks: str | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + ): + result = await client.get_price_input( + token0, token1, qty, fee, tick_spacing, hooks, hook_data, route + ) + assert result + + # Output quotes + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_get_quote_exact_output_single( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.get_quote_exact_output_single( + token0, token1, qty, fee, tick_spacing, hooks + ) + assert result + + @pytest.mark.parametrize( + "token0, qty, route", + [ + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + ], + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + ), + ], + ) + @pytest.mark.asyncio + async def test_get_quote_exact_output( + self, client: AsyncUniswap4, token0: str, qty: int, route: list[PoolKey] + ): + result = await client.get_quote_exact_output(token0, qty, route) + assert result + + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, route", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + 1000 * ONE_USDT, + None, + None, + None, + None, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + None, + None, + None, + None, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + ), + ], + ) + @pytest.mark.asyncio + async def test_get_price_output( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int | None, + tick_spacing: int | None, + hooks: str | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + ): + result = await client.get_price_output( + token0, token1, qty, fee, tick_spacing, hooks, hook_data, route + ) + assert result + + # Price impact/spot price + @pytest.mark.parametrize( + "token0, token1, test_volume", + [ + (ETH_ADDRESS, USDC_ADDRESS, ONE_ETH), + (ETH_ADDRESS, USDC_ADDRESS, 10 * ONE_ETH), + (ETH_ADDRESS, USDC_ADDRESS, 100 * ONE_ETH), + (ETH_ADDRESS, USDC_ADDRESS, 1000 * ONE_ETH), + ], + ) + @pytest.mark.asyncio + async def test_estimate_price_impact( + self, client: AsyncUniswap4, token0: str, token1: str, test_volume: int + ): + result = await client.estimate_price_impact(token0, token1, test_volume) + assert result + + @pytest.mark.parametrize( + "token0, token1", + [ + (ETH_ADDRESS, USDC_ADDRESS), + (USDC_ADDRESS, ETH_ADDRESS), + ], + ) + @pytest.mark.asyncio + async def test_get_token_token_spot_price( + self, client: AsyncUniswap4, token0: str, token1: str + ): + result = await client.get_token_token_spot_price(token0, token1) + assert result + + # ------ Swaps---------------------------------------------------------------------- + # Input swaps + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_token_to_token_swap_exact_input( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + qtycap = await client.get_quote_exact_input_single( + token0, token1, qty, fee, tick_spacing, hooks, hook_data + ) + tx = await client.token_to_token_swap_exact_input( + token0, + qty, + qtycap, + token1, + fee, + tick_spacing, + hooks, + hook_data, + 0, # min_hop_price_x_36 + custom_nonce, + ) + assert tx + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, qty, route, custom_nonce", + [ + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + ], + None, + ), + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + None, + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_token_to_token_swap_input( + self, + client: AsyncUniswap4, + token0: str, + qty: int, + route: list[PoolKey], + custom_nonce: Nonce | None, + ): + qtycap = await client.get_quote_exact_input(token0, qty, route) + + tx = await client.token_to_token_swap_input( + token0, qty, qtycap, route, min_hop_price_x_36=[], custom_nonce=custom_nonce + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, token1, qty, pool_key, hook_data, route, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 10 * ONE_ETH, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDC, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + 10 * ONE_ETH, + None, + None, + [eth_usdc_poolkey, usdc_usdt_poolkey], + None, + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDT, + None, + None, + [usdc_usdt_poolkey, eth_usdc_poolkey], + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_make_swap_input( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + pool_key: PoolKey | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + custom_nonce: Nonce | None, + ): + qtycap = await client.get_price_input( + token0, + token1, + qty, + None if pool_key is None else pool_key.fee, + None if pool_key is None else pool_key.tick_spacing, + None if pool_key is None else pool_key.hooks, + hook_data, + route, + ) + tx = await client.make_swap_input( + token0, token1, qty, qtycap, pool_key, hook_data, route, custom_nonce + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # Output swaps + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_token_to_token_swap_exact_output( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + qtycap = await client.get_quote_exact_output_single( + token0, token1, qty, fee, tick_spacing, hooks, hook_data + ) + tx = await client.token_to_token_swap_exact_output( + token0, + qty, + qtycap, + token1, + fee, + tick_spacing, + hooks, + hook_data, + 0, # min_hop_price_x_36 + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, qty, route, custom_nonce", + [ + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + None, + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_token_to_token_swap_output( + self, + client: AsyncUniswap4, + token0: str, + qty: int, + route: list[PoolKey], + custom_nonce: Nonce | None, + ): + qtycap = await client.get_quote_exact_output(token0, qty, route) + tx = await client.token_to_token_swap_output( + token0, qty, qtycap, route, min_hop_price_x_36=[], custom_nonce=custom_nonce + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, token1, qty, pool_key, hook_data, route, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + 1000 * ONE_USDT, + None, + None, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + None, + None, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_make_swap_output( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + pool_key: PoolKey | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + custom_nonce: Nonce | None, + ): + qtycap = await client.get_price_output( + token0, + token1, + qty, + None if pool_key is None else pool_key.fee, + None if pool_key is None else pool_key.tick_spacing, + None if pool_key is None else pool_key.hooks, + hook_data, + route, + ) + tx = await client.make_swap_output( + token0, token1, qty, qtycap, pool_key, hook_data, route, custom_nonce + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # ------ Liquidity -------------------------------------------------------------------- + + @pytest.mark.parametrize( + "pool_key, custom_nonce", + [ + (eth_usdc_poolkey, None), + ], + ) + @pytest.mark.asyncio + async def test_create_pool( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + custom_nonce: Nonce | None, + ): + sqrt_price_x96 = 1 << 96 # 1:1 price + test_pool_key = PoolKey( + currency0=pool_key.currency0, + currency1=pool_key.currency1, + fee=pool_key.fee + 100, + tick_spacing=pool_key.tick_spacing + 500, + hooks=pool_key.hooks, + ) + tx = await client.create_pool(test_pool_key, sqrt_price_x96, custom_nonce) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "pool_key, tick_lower, tick_upper, liquidity, amount0, amount1, hook_data, custom_nonce", + [ + ( + eth_usdc_poolkey, + -600, + 600, + ONE_ETH, + ONE_ETH, + 2500 * ONE_USDC, + b"", + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_mint_position( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + tick_lower: int, + tick_upper: int, + liquidity: int, + amount0: int, + amount1: int, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = await client.mint_position( + pool_key, + tick_lower, + tick_upper, + liquidity, + amount0, + amount1, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + test_token_id_result = await client.get_minted_token_id( + tx_receipt["transactionHash"].hex() + ) + assert len(test_token_id_result) > 0 + TestAsyncUniswap4.test_token_id = test_token_id_result[0] + TestAsyncUniswap4.test_mint_tx_hash = tx_receipt["transactionHash"].hex() + + @pytest.mark.asyncio + async def test_get_minted_token_id( + self, + client: AsyncUniswap4, + ): + result = await client.get_minted_token_id(TestAsyncUniswap4.test_mint_tx_hash) + assert result + + @pytest.mark.asyncio + async def test_get_position_info( + self, + client: AsyncUniswap4, + ): + result = await client.get_position_info(TestAsyncUniswap4.test_token_id) + test_pool_id_result: int = int.from_bytes(result["poolID"], byteorder="big") + truncated_pool_id_str = f"{test_pool_id_result:x}".lower() + pool_id_str = ( + client.get_pool_id( + PoolKey( + result["currency0"], + result["currency1"], + result["fee"], + result["tickSpacing"], + result["hooks"], + ) + ) + .hex() + .lower() + ) + assert truncated_pool_id_str == pool_id_str[: len(truncated_pool_id_str)] + + @pytest.mark.parametrize( + "token0_decimals, token1_decimals", + [(ETH_DECIMALS, USDC_DECIMALS)], + ) + @pytest.mark.asyncio + async def test_get_position_value( + self, + client: AsyncUniswap4, + token0_decimals: int, + token1_decimals: int, + ): + result = await client.get_position_value( + TestAsyncUniswap4.test_token_id, token0_decimals, token1_decimals + ) + assert result + + @pytest.mark.parametrize( + "pool_key, liquidity, amount0, amount1, hook_data, custom_nonce", + [ + ( + eth_usdc_poolkey, + int(0.05 * ONE_ETH), + int(0.5 * ONE_ETH), + 1250 * ONE_USDC, + b"", + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_increase_liquidity( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + liquidity: int, + amount0: int, + amount1: int, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = await client.increase_liquidity( + pool_key, + TestAsyncUniswap4.test_token_id, + amount0, + amount1, + liquidity, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "pool_key, hook_data, custom_nonce", + [ + (eth_usdc_poolkey, b"", None), + ], + ) + @pytest.mark.asyncio + async def test_collect_fees( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = await client.collect_fees( + pool_key, + TestAsyncUniswap4.test_token_id, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "pool_key, liquidity, amount0, amount1, hook_data, custom_nonce", + [ + ( + eth_usdc_poolkey, + int(0.05 * ONE_ETH), + 0, + 0, + b"", + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_decrease_liquidity( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + liquidity: int, + amount0: int, + amount1: int, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = await client.decrease_liquidity( + pool_key, + TestAsyncUniswap4.test_token_id, + amount0, + amount1, + liquidity, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # ------ V4Pools tests ---------------------------------------------------------------- + # @pytest.mark.skip( + # reason="Test node issues with fetching poolkey data, skipping for now" + # ) + @pytest.mark.asyncio + @pytest.mark.parametrize( + "first_block", + [ + (21688329), + ], + ) + async def test_fetch_poolkey_data( + self, + pool_service: AsyncV4pools, + first_block: int, + ): + result: int = await pool_service.fetch_poolkey_data( + first_block, + chunk_size=5, + clear_list=False, + retry_attempts=1, + minutes_between_retries=1, + last_block=first_block + 101, + ) + if result != 0: + pytest.skip( + f"Failed to fetch poolkey data, connection issues, result: {result}" + ) + assert result == 0 + + @pytest.mark.parametrize( + "test_data_file_path", + [ + "pool_list.test", + ], + ) + def test_load_poolkeys_list( + self, + pool_service: V4pools, + test_data_file_path: str, + ): + test_data_file_full_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), test_data_file_path + ) + pool_service.load_poolkeys_list(test_data_file_full_path) + assert len(pool_service.poolkeys_list) > 0 + + @pytest.mark.parametrize( + "test_data_file_path", + [ + "pool_list_dump.test", + ], + ) + def test_save_poolkeys_list( + self, + pool_service: V4pools, + test_data_file_path: str, + tmp_path, + ): + test_data_file_full_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "pool_list.test" + ) + pool_service.load_poolkeys_list(test_data_file_full_path) + temp_dir = tmp_path / "temp" + temp_dir.mkdir() + test_data_dump_full_path = temp_dir / test_data_file_path + pool_service.save_poolkeys_list(test_data_dump_full_path) + assert len(test_data_dump_full_path.read_text()) > 0 + + @pytest.mark.parametrize( + "currecy0, currency1", + [ + (ETH_ADDRESS, USDC_ADDRESS), + (USDC_ADDRESS, USDT_ADDRESS), + ], + ) + def test_get_poolkeys_sublist( + self, + pool_service: V4pools, + currecy0: str, + currency1: str, + ): + test_data_file_full_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "pool_list.test" + ) + pool_service.load_poolkeys_list(test_data_file_full_path) + + result = pool_service.get_poolkeys_sublist(currecy0, currency1) + assert len(result) > 0 + + # ------ StateView tests -------------------------------------------------------------- + # Read methods + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_liquidity( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_liquidity( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_slot0( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_slot0( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + assert result + TestAsyncUniswap4.test_tick = int(result["tick"]) + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_fee_growth_globals( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_fee_growth_globals( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_fee_growth_inside( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_fee_growth_inside( + token0, + token1, + fee, + tick_spacing, + hooks, + TestAsyncUniswap4.test_tick - tick_spacing * 50, + TestAsyncUniswap4.test_tick + tick_spacing * 50, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_position_info( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_position_info( + token0, + token1, + fee, + tick_spacing, + hooks, + _addr_to_str(client.address), + -600, + 600, + TestAsyncUniswap4.test_token_id, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_tick_bitmap( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_tick_bitmap( + token0, + token1, + fee, + tick_spacing, + hooks, + -30000, + ) + assert isinstance(result, int) + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_tick_fee_growth_outside( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_tick_fee_growth_outside( + token0, + token1, + fee, + tick_spacing, + hooks, + TestAsyncUniswap4.test_tick, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_tick_pool_info( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_tick_pool_info( + token0, + token1, + fee, + tick_spacing, + hooks, + TestAsyncUniswap4.test_tick, + ) + assert result + + # ------ PositionDescriptor tests ----------------------------------------------------- + # Read methods + @pytest.mark.parametrize( + "token0", + [ + (USDC_ADDRESS), + (USDT_ADDRESS), + ], + ) + @pytest.mark.asyncio + async def test_position_descriptor_get_currency_ratio_priority( + self, client: AsyncUniswap4, token0: str + ): + result = await client.position_descriptor_get_currency_ratio_priority(token0) + assert isinstance(result, int) + + @pytest.mark.parametrize( + "token0, token1", + [ + (USDC_ADDRESS, ETH_ADDRESS), + (USDT_ADDRESS, ETH_ADDRESS), + ], + ) + @pytest.mark.asyncio + async def test_position_descriptor_get_flip_ratio( + self, client: AsyncUniswap4, token0: str, token1: str + ): + result = await client.position_descriptor_get_flip_ratio(token0, token1) + assert isinstance(result, bool) + + @pytest.mark.asyncio + async def test_position_descriptor_get_native_currency_label( + self, client: AsyncUniswap4 + ): + result = await client.position_descriptor_get_native_currency_label() + assert result + + @pytest.mark.asyncio + async def test_position_descriptor_get_pool_manager(self, client: AsyncUniswap4): + result = await client.position_descriptor_get_pool_manager() + assert result + + @pytest.mark.asyncio + async def test_position_descriptor_get_token_uri(self, client: AsyncUniswap4): + result = await client.position_descriptor_get_token_uri( + _addr_to_str(client.position_manager_address), + TestAsyncUniswap4.test_token_id, + ) + assert result + + @pytest.mark.asyncio + async def test_position_descriptor_get_wrapped_native_address( + self, client: AsyncUniswap4 + ): + result = await client.position_descriptor_get_wrapped_native_address() + assert result + + # ------ PositionManager tests -------------------------------------------------------- + # Read methods + + # Write methods + + # ------ PoolManager tests ------------------------------------------------------------ + # Read methods + + # Write methods + + # Burn test position. + @pytest.mark.parametrize( + "pool_key, hook_data, custom_nonce", + [ + (eth_usdc_poolkey, b"", None), + ], + ) + @pytest.mark.asyncio + async def test_burn_position( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + # Removing liquidity before burning position, otherwise burn will revert since position is not empty + tx = await client.decrease_liquidity( + pool_key, + TestAsyncUniswap4.test_token_id, + 0, + 0, + ( + await client.stateview_get_position_info( + pool_key.currency0, + pool_key.currency1, + pool_key.fee, + pool_key.tick_spacing, + pool_key.hooks, + recipient, + -600, + 600, + TestAsyncUniswap4.test_token_id, + ) + )["liquidity"], + recipient, + hook_data, + custom_nonce, + ) + assert tx + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # Now burn the position + tx = await client.burn_position( + pool_key, + TestAsyncUniswap4.test_token_id, + 0, + 0, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # ------ UniversalRouter tests -------------------------------------------------------- + @pytest.mark.asyncio + async def test_universal_router_execute_simple_command( + self, + client: AsyncUniswap4, + ): + commands: list = [ + universal_router_commands["WRAP_ETH"], + ] + actions: list = [ + [], + ] + params: list = [ + [ + [ + _addr_to_str(client.address), + 1 * ONE_ETH, + ], + ], + ] + tx = await client.universal_router_execute( + commands, actions, params, ether_amount=1 * ONE_ETH + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.asyncio + async def test_universal_router_execute_composite_command( + self, + client: AsyncUniswap4, + ): + if eth_usdc_poolkey.currency0.lower() < eth_usdc_poolkey.currency1.lower(): + zero_for_one: bool = True + else: + zero_for_one = False + qty: int = 1 * ONE_ETH + qtycap: int = await client.get_quote_exact_input_single( + eth_usdc_poolkey.currency0, + eth_usdc_poolkey.currency1, + qty, + eth_usdc_poolkey.fee, + eth_usdc_poolkey.tick_spacing, + eth_usdc_poolkey.hooks, + b"", + ) + qtycap = int((1 - client.max_slippage) * qtycap) + + commands: list = [ + universal_router_commands["V4_SWAP"], + ] + actions: list = [ + [ + v4_actions["SWAP_EXACT_IN_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ] + + params: list = [ + [ + [ + ( + astuple(eth_usdc_poolkey), + zero_for_one, + qty, + qtycap, + 0, # min_hop_price_x_36 + b"", + ) + ], + [ + eth_usdc_poolkey.currency0, + qty, + ], + [ + eth_usdc_poolkey.currency1, + qtycap, + ], + ], + ] + tx = await client.universal_router_execute( + commands, actions, params, ether_amount=qty + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.asyncio + async def test_universal_router_execute_multiaction( + self, + client: AsyncUniswap4, + ): + if eth_usdc_poolkey.currency0.lower() < eth_usdc_poolkey.currency1.lower(): + zero_for_one: bool = True + else: + zero_for_one = False + qty: int = 1 * ONE_ETH + qtycap: int = await client.get_quote_exact_input_single( + eth_usdc_poolkey.currency0, + eth_usdc_poolkey.currency1, + qty, + eth_usdc_poolkey.fee, + eth_usdc_poolkey.tick_spacing, + eth_usdc_poolkey.hooks, + b"", + ) + qtycap = int((1 - client.max_slippage) * qtycap) + ether_value = ONE_ETH + + # Executes a WRAP_ETH followed by a V4_SWAP in the same transaction + commands: list = [ + universal_router_commands["WRAP_ETH"], + universal_router_commands["V4_SWAP"], + ] + + # List of actions for each command, in this case we have 0 action for the WRAP_ETH command and 3 actions for the V4_SWAP command (swap, settle and take) + # As WRAP_ETH command does not require any action, we pass an empty list for it + actions: list = [ + [], + [ + v4_actions["SWAP_EXACT_IN_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ] + + # List of parameters for each action, the first element of the list corresponds to the parameters for the WRAP_ETH command (recipient and amount), + # and the second element corresponds to the parameters for each of the 3 actions of the V4_SWAP command + params: list = [ + [ + [ + _addr_to_str(client.address), + ether_value, + ], + ], + [ + [ + ( + astuple(eth_usdc_poolkey), + zero_for_one, + qty, + qtycap, + 0, # min_hop_price_x_36 + b"", + ) + ], + [ + eth_usdc_poolkey.currency0, + qty, + ], + [ + eth_usdc_poolkey.currency1, + qtycap, + ], + ], + ] + # Both commands use ether as input token, so we set the ether_amount to the sum of the ether required for each commands + tx = await client.universal_router_execute( + commands, actions, params, ether_amount=(ether_value + qty) + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 9b3bbf1c..5abf248e 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -2,10 +2,10 @@ import os import shutil import subprocess +from collections.abc import Generator from contextlib import contextmanager from dataclasses import dataclass from time import sleep -from typing import Generator import pytest from web3 import Web3 @@ -47,17 +47,17 @@ @dataclass -class GanacheInstance: +class AnvilInstance: provider: str eth_address: str eth_privkey: str @pytest.fixture(scope="module", params=UNISWAP_VERSIONS) -def client(request, web3: Web3, ganache: GanacheInstance): +def client(request, web3: Web3, anvil: AnvilInstance): return Uniswap( - ganache.eth_address, - ganache.eth_privkey, + anvil.eth_address, + anvil.eth_privkey, web3=web3, version=request.param, use_estimate_gas=False, # see note in _build_and_send_tx @@ -95,45 +95,42 @@ def test_assets(client: Uniswap): @pytest.fixture(scope="module") -def web3(ganache: GanacheInstance): - w3 = Web3(Web3.HTTPProvider(ganache.provider, request_kwargs={"timeout": 30})) +def web3(anvil: AnvilInstance): + w3 = Web3(Web3.HTTPProvider(anvil.provider, request_kwargs={"timeout": 30})) if 1 != int(w3.net.version): logger.warning("PROVIDER was not a mainnet provider, which the tests require") return w3 @pytest.fixture(scope="module") -def ganache() -> Generator[GanacheInstance, None, None]: - """Fixture that runs ganache which has forked off mainnet""" - if not shutil.which("ganache"): - raise Exception( - "ganache was not found in PATH, you can install it with `npm install -g ganache`" +def anvil() -> Generator[AnvilInstance, None, None]: + """Fixture that runs anvil which has forked off mainnet""" + if not shutil.which("anvil"): + raise ValueError( + "anvil was not found in PATH, install Foundry: https://getfoundry.sh" ) if "PROVIDER" not in os.environ: - raise Exception( + raise ValueError( "PROVIDER was not set, you need to set it to a mainnet provider (such as Infura) so that we can fork off our testnet" ) port = 10999 defaultGasPrice = 100_000_000_000 # 100 gwei p = subprocess.Popen( - f"""ganache + f"""anvil --port {port} - --wallet.seed test - --chain.networkId 1 - --chain.chainId 1 - --fork.url {os.environ["PROVIDER"]} - --miner.defaultGasPrice {defaultGasPrice} - --miner.instamine "strict" + --chain-id 1 + --fork-url {os.environ["PROVIDER"]} + --gas-price {defaultGasPrice} """.replace("\n", " "), shell=True, ) - # Address #1 when ganache is run with `--wallet.seed test`, it starts with 1000 ETH - eth_address = "0x94e3361495bD110114ac0b6e35Ed75E77E6a6cFA" - eth_privkey = "0x6f1313062db38875fb01ee52682cbf6a8420e92bfbc578c5d4fdc0a32c50266f" + # Account #9 from anvil's default test mnemonic, starts with 1000 ETH + eth_address = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" + eth_privkey = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" sleep(3) - yield GanacheInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) + yield AnvilInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) p.kill() p.wait() @@ -151,7 +148,7 @@ def does_not_raise(): # TODO: Change pytest.param(..., mark=pytest.mark.xfail) to the expectation/raises method @pytest.mark.usefixtures("client", "web3") -class TestUniswap(object): +class TestUniswap: # ------ Exchange ------------------------------------------------------------------ def test_get_fee_maker(self, client: Uniswap): if client.version not in [1, 2]: @@ -166,6 +163,22 @@ def test_get_fee_taker(self, client: Uniswap): assert r == 0.003 # ------ Market -------------------------------------------------------------------- + @pytest.mark.parametrize( + "token0", + [ + ("ETH"), + ("UNI"), + ("DAI"), + ("USDC"), + ], + ) + def test_approve(self, client: Uniswap, tokens, token0): + token0 = tokens[token0] + if client.version == 1 and ETH_ADDRESS == token0: + pytest.skip("Not supported in this version of Uniswap") + r = client.approve(token0) + assert r + @pytest.mark.parametrize( "token0, token1, qty", [ @@ -285,7 +298,7 @@ def test_get_ex_eth_balance( tokens, token, ): - if not client.version == 1: + if client.version != 1: pytest.skip("Only supported on Uniswap v1") r = client.get_ex_eth_balance(tokens[token]) assert r @@ -297,7 +310,7 @@ def test_get_ex_token_balance( tokens, token, ): - if not client.version == 1: + if client.version != 1: pytest.skip("Only supported on Uniswap v1") r = client.get_ex_token_balance(tokens[token]) assert r @@ -309,7 +322,7 @@ def test_get_exchange_rate( tokens, token, ): - if not client.version == 1: + if client.version != 1: pytest.skip("Only supported on Uniswap v1") r = client.get_exchange_rate(tokens[token]) assert r @@ -446,7 +459,7 @@ def test_remove_liquidity( # Token -> Token ("DAI", "USDC", ONE_ETH, None, does_not_raise), # Token -> ETH - ("USDC", "ETH", ONE_USDC, None, does_not_raise), + ("USDC", "ETH", ONE_USDC // 2, None, does_not_raise), # ("ETH", "UNI", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), # ("UNI", "ETH", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), # ("DAI", "UNI", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), @@ -469,6 +482,14 @@ def test_make_trade( pytest.skip( "Not supported in this version of Uniswap, or at least no liquidity" ) + # Uniswap v1 token-to-ETH uses Vyper 0.1.x bytecode with non-standard JUMP + # patterns that Ganache tolerated but Anvil's strict revm rejects (InvalidJump). + # xfail until v1 is formally deprecated or a compatible fork-mode is found. + if client.version == 1 and output_token == ETH_ADDRESS: + pytest.xfail( + "v1 token-to-ETH: EvmError: InvalidJump — Vyper 0.1.x bytecode " + "incompatible with Anvil revm; tracked for v1 deprecation" + ) with expectation(): bal_in_before = client.get_token_balance(input_token) @@ -516,6 +537,12 @@ def test_make_trade_output( pytest.skip( "Not supported in this version of Uniswap, or at least no liquidity" ) + # Same Anvil revm InvalidJump for v1 token-to-ETH (see test_make_trade above). + if client.version == 1 and output_token == ETH_ADDRESS: + pytest.xfail( + "v1 token-to-ETH: EvmError: InvalidJump — Vyper 0.1.x bytecode " + "incompatible with Anvil revm; tracked for v1 deprecation" + ) with expectation(): balance_before = client.get_token_balance(output_token) diff --git a/tests/test_uniswap4.py b/tests/test_uniswap4.py index 22cbaf46..89cabc81 100644 --- a/tests/test_uniswap4.py +++ b/tests/test_uniswap4.py @@ -2,10 +2,10 @@ import os import shutil import subprocess +from collections.abc import Generator from contextlib import contextmanager from dataclasses import astuple, dataclass from time import sleep -from typing import Generator, List, Optional import pytest from web3 import Web3 @@ -98,9 +98,9 @@ def web3(anvil: AnvilInstance) -> Web3: def anvil() -> Generator[AnvilInstance, None, None]: """Fixture that runs anvil which has forked off mainnet""" if not shutil.which("anvil"): - raise Exception("anvil was not found in PATH") + raise ValueError("anvil was not found in PATH") if "PROVIDER" not in os.environ: - raise Exception( + raise ValueError( "PROVIDER was not set, you need to set it to a mainnet provider (such as Infura) so that we can fork off our testnet" ) @@ -130,7 +130,7 @@ def does_not_raise(): @pytest.mark.usefixtures("client", "web3") -class TestUniswap4(object): +class TestUniswap4: test_token_id: int = 0 test_mint_tx_hash: str = "" test_tick: int = 0 @@ -147,7 +147,7 @@ def test_approve( self, client: Uniswap4, token: str, - max_approval: Optional[int], + max_approval: int | None, delay_interval: int, ): # Approve the token @@ -181,7 +181,7 @@ def test_drop_txn( address_to: str, gas_price: float, priority_fee: int, - custom_nonce: Optional[int], + custom_nonce: int | None, ): if not client.w3.is_address(address_to): address: AddressLike = client.address @@ -189,7 +189,7 @@ def test_drop_txn( address = _str_to_addr(address_to) client.update_last_nonce() if custom_nonce == 0: - nonce: Optional[Nonce] = client.last_nonce + nonce: Nonce | None = client.last_nonce else: nonce = None @@ -286,7 +286,7 @@ def test_get_quote_exact_input( client: Uniswap4, token0: str, qty: int, - route: List[PoolKey], + route: list[PoolKey], ): result = client.get_quote_exact_input(token0, qty, route) assert result @@ -342,11 +342,11 @@ def test_get_price_input( token0: str, token1: str, qty: int, - fee: Optional[int], - tick_spacing: Optional[int], - hooks: Optional[str], - hook_data: Optional[bytes], - route: Optional[List[PoolKey]], + fee: int | None, + tick_spacing: int | None, + hooks: str | None, + hook_data: bytes | None, + route: list[PoolKey] | None, ): result = client.get_price_input( token0, token1, qty, fee, tick_spacing, hooks, hook_data, route @@ -426,7 +426,7 @@ def test_get_quote_exact_output_single( ], ) def test_get_quote_exact_output( - self, client: Uniswap4, token0: str, qty: int, route: List[PoolKey] + self, client: Uniswap4, token0: str, qty: int, route: list[PoolKey] ): result = client.get_quote_exact_output(token0, qty, route) assert result @@ -488,11 +488,11 @@ def test_get_price_output( token0: str, token1: str, qty: int, - fee: Optional[int], - tick_spacing: Optional[int], - hooks: Optional[str], - hook_data: Optional[bytes], - route: Optional[List[PoolKey]], + fee: int | None, + tick_spacing: int | None, + hooks: str | None, + hook_data: bytes | None, + route: list[PoolKey] | None, ): result = client.get_price_output( token0, token1, qty, fee, tick_spacing, hooks, hook_data, route @@ -565,7 +565,7 @@ def test_token_to_token_swap_exact_input( tick_spacing: int, hooks: str, hook_data: bytes, - custom_nonce: Optional[Nonce], + custom_nonce: Nonce | None, ): qtycap = client.get_quote_exact_input_single( token0, token1, qty, fee, tick_spacing, hooks, hook_data @@ -634,8 +634,8 @@ def test_token_to_token_swap_input( client: Uniswap4, token0: str, qty: int, - route: List[PoolKey], - custom_nonce: Optional[Nonce], + route: list[PoolKey], + custom_nonce: Nonce | None, ): qtycap = client.get_quote_exact_input(token0, qty, route) @@ -698,10 +698,10 @@ def test_make_swap_input( token0: str, token1: str, qty: int, - pool_key: Optional[PoolKey], - hook_data: Optional[bytes], - route: Optional[List[PoolKey]], - custom_nonce: Optional[Nonce], + pool_key: PoolKey | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + custom_nonce: Nonce | None, ): qtycap = client.get_price_input( token0, @@ -761,7 +761,7 @@ def test_token_to_token_swap_exact_output( tick_spacing: int, hooks: str, hook_data: bytes, - custom_nonce: Optional[Nonce], + custom_nonce: Nonce | None, ): qtycap = client.get_quote_exact_output_single( token0, token1, qty, fee, tick_spacing, hooks, hook_data @@ -831,8 +831,8 @@ def test_token_to_token_swap_output( client: Uniswap4, token0: str, qty: int, - route: List[PoolKey], - custom_nonce: Optional[Nonce], + route: list[PoolKey], + custom_nonce: Nonce | None, ): qtycap = client.get_quote_exact_output(token0, qty, route) tx = client.token_to_token_swap_output( @@ -900,10 +900,10 @@ def test_make_swap_output( token0: str, token1: str, qty: int, - pool_key: Optional[PoolKey], - hook_data: Optional[bytes], - route: Optional[List[PoolKey]], - custom_nonce: Optional[Nonce], + pool_key: PoolKey | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + custom_nonce: Nonce | None, ): qtycap = client.get_price_output( token0, @@ -939,7 +939,7 @@ def test_create_pool( self, client: Uniswap4, pool_key: PoolKey, - custom_nonce: Optional[Nonce], + custom_nonce: Nonce | None, ): sqrt_price_x96 = 1 << 96 # 1:1 price test_pool_key = PoolKey( @@ -984,7 +984,7 @@ def test_mint_position( amount0: int, amount1: int, hook_data: bytes, - custom_nonce: Optional[Nonce], + custom_nonce: Nonce | None, ): recipient = _addr_to_str(client.address) tx = client.mint_position( @@ -1026,7 +1026,7 @@ def test_get_position_info( ): result = client.get_position_info(TestUniswap4.test_token_id) test_pool_id_result: int = int.from_bytes(result["poolID"], byteorder="big") - truncated_pool_id_str = hex(test_pool_id_result).lower() + truncated_pool_id_str = f"{test_pool_id_result:x}".lower() pool_id_str = ( client.get_pool_id( PoolKey( @@ -1078,7 +1078,7 @@ def test_increase_liquidity( amount0: int, amount1: int, hook_data: bytes, - custom_nonce: Optional[Nonce], + custom_nonce: Nonce | None, ): recipient = _addr_to_str(client.address) tx = client.increase_liquidity( @@ -1111,7 +1111,7 @@ def test_collect_fees( client: Uniswap4, pool_key: PoolKey, hook_data: bytes, - custom_nonce: Optional[Nonce], + custom_nonce: Nonce | None, ): recipient = _addr_to_str(client.address) tx = client.collect_fees( @@ -1147,7 +1147,7 @@ def test_decrease_liquidity( amount0: int, amount1: int, hook_data: bytes, - custom_nonce: Optional[Nonce], + custom_nonce: Nonce | None, ): recipient = _addr_to_str(client.address) tx = client.decrease_liquidity( @@ -1170,8 +1170,11 @@ def test_decrease_liquidity( ) # ------ V4Pools tests ---------------------------------------------------------------- - @pytest.mark.skip( - reason="Test node issues with fetching poolkey data, skipping for now" + @pytest.mark.parametrize( + "first_block", + [ + (21688329), + ], ) def test_fetch_poolkey_data( self, @@ -1179,8 +1182,17 @@ def test_fetch_poolkey_data( first_block: int, ): result: int = pool_service.fetch_poolkey_data( - first_block, chunk_size=500, clear_list=False, last_block=first_block + 1001 - ) + first_block, + chunk_size=5, + clear_list=False, + retry_attempts=1, + minutes_between_retries=1, + last_block=first_block + 101, + ) + if result != 0: + pytest.skip( + f"Failed to fetch poolkey data, connection issues, result: {result}" + ) assert result == 0 @pytest.mark.parametrize( @@ -1584,7 +1596,7 @@ def test_burn_position( client: Uniswap4, pool_key: PoolKey, hook_data: bytes, - custom_nonce: Optional[Nonce], + custom_nonce: Nonce | None, ): recipient = _addr_to_str(client.address) # Removing liquidity before burning position, otherwise burn will revert since position is not empty @@ -1640,13 +1652,13 @@ def test_universal_router_execute_simple_command( self, client: Uniswap4, ): - commands: List = [ + commands: list = [ universal_router_commands["WRAP_ETH"], ] - actions: List = [ + actions: list = [ [], ] - params: List = [ + params: list = [ [ [ _addr_to_str(client.address), @@ -1686,10 +1698,10 @@ def test_universal_router_execute_composite_command( ) qtycap = int((1 - client.max_slippage) * qtycap) - commands: List = [ + commands: list = [ universal_router_commands["V4_SWAP"], ] - actions: List = [ + actions: list = [ [ v4_actions["SWAP_EXACT_IN_SINGLE"], v4_actions["SETTLE_ALL"], @@ -1697,7 +1709,7 @@ def test_universal_router_execute_composite_command( ], ] - params: List = [ + params: list = [ [ [ ( @@ -1753,14 +1765,14 @@ def test_universal_router_execute_multiaction( ether_value = ONE_ETH # Executes a WRAP_ETH followed by a V4_SWAP in the same transaction - commands: List = [ + commands: list = [ universal_router_commands["WRAP_ETH"], universal_router_commands["V4_SWAP"], ] # List of actions for each command, in this case we have 0 action for the WRAP_ETH command and 3 actions for the V4_SWAP command (swap, settle and take) # As WRAP_ETH command does not require any action, we pass an empty list for it - actions: List = [ + actions: list = [ [], [ v4_actions["SWAP_EXACT_IN_SINGLE"], @@ -1771,7 +1783,7 @@ def test_universal_router_execute_multiaction( # List of parameters for each action, the first element of the list corresponds to the parameters for the WRAP_ETH command (recipient and amount), # and the second element corresponds to the parameters for each of the 3 actions of the V4_SWAP command - params: List = [ + params: list = [ [ [ _addr_to_str(client.address), diff --git a/tests/test_v4_examples.py b/tests/test_v4_examples.py new file mode 100644 index 00000000..4ef4d019 --- /dev/null +++ b/tests/test_v4_examples.py @@ -0,0 +1,329 @@ +""" +Runnable examples for Uniswap v4. + +These tests serve as executable documentation: each test demonstrates one +common workflow against a live mainnet fork (Anvil). They run as part of the +``UNISWAP_VERSION=4`` CI matrix job, so every snippet is verified on every +commit. + +Run locally: + PROVIDER= UNISWAP_VERSION=4 pytest tests/test_v4_examples.py -v + +See docs/v4.rst for the prose version of these examples. +""" + +import os +import shutil +import subprocess +from collections.abc import Generator +from dataclasses import dataclass +from time import sleep + +import pytest +from web3 import Web3 + +from uniswap import Uniswap4 +from uniswap.constants import ETH_ADDRESS, ZERO_HOOK +from uniswap.types import PoolKey +from uniswap.util import V4pools + +pytestmark = pytest.mark.skipif( + os.getenv("UNISWAP_VERSION") != "4", + reason="Run with UNISWAP_VERSION=4", +) + +# --------------------------------------------------------------------------- +# Well-known mainnet addresses used in examples +# --------------------------------------------------------------------------- + +USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" +USDT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7" + +# Canonical fee tiers and tick spacings +ETH_USDC_FEE = 500 +ETH_USDC_TICK_SPACING = 10 +USDC_USDT_FEE = 100 +USDC_USDT_TICK_SPACING = 1 + +ONE_ETH = 10**18 +ONE_USDC = 10**6 +ONE_USDT = 10**6 + +# PoolKey for ETH/USDC 0.05% +ETH_USDC_POOL = PoolKey( + currency0=ETH_ADDRESS, + currency1=USDC_ADDRESS, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + hooks=ZERO_HOOK, +) + +# PoolKey for USDC/USDT 0.01% +USDC_USDT_POOL = PoolKey( + currency0=USDC_ADDRESS, + currency1=USDT_ADDRESS, + fee=USDC_USDT_FEE, + tick_spacing=USDC_USDT_TICK_SPACING, + hooks=ZERO_HOOK, +) + + +# --------------------------------------------------------------------------- +# Fixtures — same pattern as test_uniswap4.py +# --------------------------------------------------------------------------- + + +@dataclass +class AnvilInstance: + provider: str + eth_address: str + eth_privkey: str + + +@pytest.fixture(scope="module") +def anvil() -> Generator[AnvilInstance, None, None]: + """Start an Anvil instance forked from mainnet.""" + if not shutil.which("anvil"): + raise ValueError("anvil was not found in PATH") + if "PROVIDER" not in os.environ: + raise ValueError( + "PROVIDER must be set to a mainnet RPC URL so Anvil can fork mainnet." + ) + + port = 10997 # different from test_uniswap4.py (10998) to allow parallel runs + p = subprocess.Popen( + f"anvil --port {port} --chain-id 1 --fork-url {os.environ['PROVIDER']}", + shell=True, + ) + # Anvil test account #2 (1000 ETH pre-funded) + eth_address = "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" + eth_privkey = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" + sleep(3) + yield AnvilInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) + p.kill() + p.wait() + + +@pytest.fixture(scope="module") +def web3(anvil: AnvilInstance) -> Web3: + return Web3(Web3.HTTPProvider(anvil.provider, request_kwargs={"timeout": 30})) + + +@pytest.fixture(scope="module") +def client(web3: Web3, anvil: AnvilInstance) -> Uniswap4: + return Uniswap4( + anvil.eth_address, + anvil.eth_privkey, + web3=web3, + gas_limit=500_000, + ) + + +@pytest.fixture(scope="module") +def pool_service(web3: Web3) -> V4pools: + return V4pools(web3) + + +# --------------------------------------------------------------------------- +# Example 1: Get the spot price +# --------------------------------------------------------------------------- + + +class TestExampleSpotPrice: + """Example: reading the current pool price from the StateView contract.""" + + def test_get_eth_usdc_spot_price(self, client: Uniswap4) -> None: + """ + get_token_token_spot_price returns the spot price of token0 in units + of token1. The result comes from the pool's sqrtPriceX96 slot without + executing any swap. + """ + price = client.get_token_token_spot_price( + token0=ETH_ADDRESS, + token1=USDC_ADDRESS, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + # Sanity-check: ETH is worth more than $100 and less than $1M + assert 100 < price < 1_000_000, f"Unexpected ETH/USDC price: {price}" + + def test_spot_price_is_reciprocal(self, client: Uniswap4) -> None: + """The USDC/ETH price should be roughly 1/ETH_USDC price.""" + eth_usdc = client.get_token_token_spot_price( + ETH_ADDRESS, USDC_ADDRESS, ETH_USDC_FEE, ETH_USDC_TICK_SPACING + ) + usdc_eth = client.get_token_token_spot_price( + USDC_ADDRESS, ETH_ADDRESS, ETH_USDC_FEE, ETH_USDC_TICK_SPACING + ) + assert abs(usdc_eth - 1.0 / eth_usdc) / (1.0 / eth_usdc) < 0.001 + + +# --------------------------------------------------------------------------- +# Example 2: Quote a trade before executing it +# --------------------------------------------------------------------------- + + +class TestExampleQuote: + """Example: use the on-chain Quoter to get an accurate trade estimate.""" + + def test_quote_exact_input_single_hop(self, client: Uniswap4) -> None: + """ + get_price_input returns the USDC received for a fixed ETH input. + Use this before a swap to set the slippage cap (qtycap). + """ + usdc_out = client.get_price_input( + token0=ETH_ADDRESS, + token1=USDC_ADDRESS, + qty=ONE_ETH, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + # Quote should be a positive integer (USDC in smallest unit) + assert usdc_out > 0 + usdc_human = usdc_out / ONE_USDC + assert 100 < usdc_human < 1_000_000, f"Unexpected USDC quote: {usdc_human}" + + def test_quote_exact_output_single_hop(self, client: Uniswap4) -> None: + """ + get_price_output returns the ETH required to buy an exact USDC amount. + """ + eth_needed = client.get_price_output( + token0=ETH_ADDRESS, + token1=USDC_ADDRESS, + qty=1000 * ONE_USDC, # buy 1000 USDC + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + assert eth_needed > 0 + assert eth_needed < ONE_ETH, "1000 USDC should cost less than 1 ETH" + + def test_quote_exact_input_two_hop(self, client: Uniswap4) -> None: + """ + Multi-hop quote: ETH → USDC → USDT via two pools. + Pass a list of PoolKey objects as ``route``. + """ + usdt_out = client.get_price_input( + token0=ETH_ADDRESS, + token1=USDT_ADDRESS, + qty=ONE_ETH // 10, # 0.1 ETH + route=[ETH_USDC_POOL, USDC_USDT_POOL], + ) + assert usdt_out > 0 + usdt_human = usdt_out / ONE_USDT + assert 10 < usdt_human < 100_000, f"Unexpected USDT quote: {usdt_human}" + + +# --------------------------------------------------------------------------- +# Example 3: Estimate price impact before swapping +# --------------------------------------------------------------------------- + + +class TestExamplePriceImpact: + """Example: check price impact to avoid trading into thin pools.""" + + def test_small_trade_has_low_impact(self, client: Uniswap4) -> None: + """ + A small ETH trade in a deep ETH/USDC pool should have near-zero impact. + estimate_price_impact returns a float: 0.01 = 1%. + """ + impact = client.estimate_price_impact( + token0=ETH_ADDRESS, + token1=USDC_ADDRESS, + qty=ONE_ETH // 100, # 0.01 ETH — tiny relative to pool depth + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + # Impact should be small (< 1%) for a tiny trade in a liquid pool + assert impact < 0.01, f"Expected low impact, got {impact:.4%}" + + def test_large_trade_has_higher_impact(self, client: Uniswap4) -> None: + """ + A larger trade should show measurably more price impact than a tiny one. + """ + small_impact = client.estimate_price_impact( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH // 100, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + large_impact = client.estimate_price_impact( + ETH_ADDRESS, + USDC_ADDRESS, + 100 * ONE_ETH, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + assert large_impact > small_impact + + +# --------------------------------------------------------------------------- +# Example 4: Read pool state via StateView +# --------------------------------------------------------------------------- + + +class TestExampleStateView: + """Example: read on-chain pool state without making a swap.""" + + def test_get_slot0(self, client: Uniswap4) -> None: + """ + stateview_get_slot0 returns the current sqrtPriceX96 and tick for a pool. + This is the cheapest way to read the current price. + """ + slot0 = client.stateview_get_slot0( + ETH_ADDRESS, + USDC_ADDRESS, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + hooks=ZERO_HOOK, + ) + assert slot0["sqrtPriceX96"] > 0 + # Tick is a signed integer; ETH/USDC tick should be negative + # (ETH address < USDC address, so token0=ETH, current price ~3400 USDC + # corresponds to a negative tick) + assert isinstance(slot0["tick"], int) + + def test_get_pool_liquidity(self, client: Uniswap4) -> None: + """ + stateview_get_liquidity returns total in-range liquidity for a pool. + A popular pool like ETH/USDC 0.05% should always have positive liquidity. + """ + liquidity = client.stateview_get_liquidity( + ETH_ADDRESS, + USDC_ADDRESS, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + hooks=ZERO_HOOK, + ) + assert liquidity > 0, "ETH/USDC 0.05% pool should have liquidity" + + +# --------------------------------------------------------------------------- +# Example 5: Discover pools for a token pair +# --------------------------------------------------------------------------- + + +class TestExamplePoolDiscovery: + """Example: filter a saved v4 pool index by token pair using V4pools.""" + + def test_find_eth_usdc_pools(self, pool_service: V4pools) -> None: + """ + Load a checked-in pool index, then filter it by token pair. + + ``fetch_poolkey_data`` is documented in ``docs/v4.rst`` but is not + suitable for the public CI RPC: scanning historical logs requires an + archive endpoint. The fixture is generated by that method and keeps + this example deterministic while exercising the filtering workflow. + """ + pool_data = os.path.join(os.path.dirname(__file__), "pool_list.test") + pool_service.load_poolkeys_list(pool_data) + + eth_usdc_pools = pool_service.get_poolkeys_sublist(ETH_ADDRESS, USDC_ADDRESS) + # There should be at least one ETH/USDC pool (the 0.05% one) + assert len(eth_usdc_pools) >= 1 + + # All returned keys should reference the correct tokens + for pk in eth_usdc_pools: + currencies = {pk.currency0.lower(), pk.currency1.lower()} + assert ETH_ADDRESS.lower() in currencies + assert USDC_ADDRESS.lower() in currencies diff --git a/uniswap/__init__.py b/uniswap/__init__.py index 6ea5aef8..f010b393 100644 --- a/uniswap/__init__.py +++ b/uniswap/__init__.py @@ -1,7 +1,19 @@ from . import exceptions +from .asyncuniswap import AsyncUniswap +from .asyncuniswap4 import AsyncUniswap4 from .cli import main from .uniswap import Uniswap, _str_to_addr from .uniswap4 import Uniswap4 -from .util import V4pools +from .util import AsyncV4pools, V4pools -__all__ = ["Uniswap", "Uniswap4", "V4pools", "exceptions", "_str_to_addr", "main"] +__all__ = [ + "AsyncUniswap", + "AsyncUniswap4", + "AsyncV4pools", + "Uniswap", + "Uniswap4", + "V4pools", + "_str_to_addr", + "exceptions", + "main", +] diff --git a/uniswap/asyncuniswap.py b/uniswap/asyncuniswap.py new file mode 100644 index 00000000..a88502b0 --- /dev/null +++ b/uniswap/asyncuniswap.py @@ -0,0 +1,2131 @@ +import asyncio +import logging +import os +import time +from collections import namedtuple +from collections.abc import Iterable, Sequence +from typing import ( + Any, +) + +from eth_typing import HexStr +from eth_typing.evm import Address, ChecksumAddress +from hexbytes import HexBytes +from web3 import AsyncWeb3, Web3 +from web3._utils.abi import map_abi_data +from web3._utils.normalizers import BASE_RETURN_NORMALIZERS +from web3.contract import AsyncContract +from web3.contract.async_contract import AsyncContractFunction +from web3.exceptions import BadFunctionCallOutput, ContractLogicError +from web3.types import ( + Nonce, + TxParams, + TxReceipt, + Wei, +) + +from .constants import ( + ETH_ADDRESS, + MAX_TICK, + MAX_UINT_128, + MIN_TICK, + WETH9_ADDRESS, + _factory_contract_addresses_v1, + _factory_contract_addresses_v2, + _netid_to_name, + _router_contract_addresses_v2, + _tick_bitmap_range, + _tick_spacing, +) +from .exceptions import InsufficientBalance, InvalidToken +from .fee import validate_fee_tier +from .token import ERC20Token +from .types import AddressLike +from .util import ( + _addr_to_str, + _load_abi, + _str_to_addr, + _validate_address, + chunks, + encode_sqrt_ratioX96, + is_same_address, + nearest_tick, + realised_fee_percentage, +) + +logger = logging.getLogger(__name__) + + +class AsyncUniswap: + """ + Wrapper around Uniswap contracts. + """ + + address: AddressLike + version: int + + w3: AsyncWeb3 + netid: int + netname: str + + default_slippage: float + use_estimate_gas: bool + + def __init__( + self, + address: AddressLike | str | None, + private_key: str | None, + provider: str | None = None, + web3: AsyncWeb3 | None = None, + version: int = 1, + default_slippage: float = 0.01, + use_estimate_gas: bool = True, + # use_eip1559: bool = True, + factory_contract_addr: str | None = None, + router_contract_addr: str | None = None, + enable_caching: bool = False, + ) -> None: + """ + :param address: The public address of the ETH wallet to use. + :param private_key: The private key of the ETH wallet to use. + :param provider: Can be optionally set to a Web3 provider URI. If none set, will fall back to the PROVIDER environment variable, or web3 if set. + :param web3: Can be optionally set to a custom Web3 instance. + :param version: Which version of the Uniswap contracts to use. + :param default_slippage: Default slippage for a trade, as a float (0.01 is 1%). WARNING: slippage is untested. + :param factory_contract_addr: Can be optionally set to override the address of the factory contract. + :param router_contract_addr: Can be optionally set to override the address of the router contract (v2 only). + :param enable_caching: Optionally enables middleware caching RPC method calls. + """ + self.address = _str_to_addr( + address or "0x0000000000000000000000000000000000000000" + ) + self.private_key = ( + private_key + or "0x0000000000000000000000000000000000000000000000000000000000000000" + ) + + self.version = version + if self.version not in [1, 2, 3]: + raise ValueError( + f"Invalid version '{self.version}', only 1, 2 or 3 supported" + ) # pragma: no cover + + # TODO: Write tests for slippage + self.default_slippage = default_slippage + self.use_estimate_gas = use_estimate_gas + + if not provider: + provider = os.environ["PROVIDER"] + if web3: + self.w3 = web3 + else: + # Initialize web3. Extra provider for testing. + self.w3 = AsyncWeb3( + AsyncWeb3.AsyncHTTPProvider(provider, request_kwargs={"timeout": 60}) + ) + tmp_w3 = Web3(Web3.HTTPProvider(provider, request_kwargs={"timeout": 60})) + self.netid = int(tmp_w3.net.version) + if self.netid in _netid_to_name: + self.netname = _netid_to_name[self.netid] + else: + raise ValueError(f"Unknown netid: {self.netid}") # pragma: no cover + logger.info(f"Using {self.w3} ('{self.netname}', netid: {self.netid})") + + self.last_nonce: Nonce = tmp_w3.eth.get_transaction_count(self.address) + + # This code automatically approves you for trading on the exchange. + # max_approval is to allow the contract to exchange on your behalf. + # max_approval_check checks that current approval is above a reasonable number + # The program cannot check for max_approval each time because it decreases + # with each trade. + max_approval_hex = f"0x{64 * 'f'}" + self.max_approval_int = int(max_approval_hex, 16) + max_approval_check_hex = f"0x{15 * '0'}{49 * 'f'}" + self.max_approval_check_int = int(max_approval_check_hex, 16) + + if self.version == 1: + if factory_contract_addr is None: + factory_contract_addr = _factory_contract_addresses_v1[self.netname] + + self.factory_contract = _load_contract( + self.w3, + abi_name="uniswap-v1/factory", + address=_str_to_addr(factory_contract_addr), + ) + elif self.version == 2: + if router_contract_addr is None: + router_contract_addr = _router_contract_addresses_v2[self.netname] + self.router_address: AddressLike = _str_to_addr(router_contract_addr) + + if factory_contract_addr is None: + factory_contract_addr = _factory_contract_addresses_v2[self.netname] + self.factory_contract = _load_contract( + self.w3, + abi_name="uniswap-v2/factory", + address=_str_to_addr(factory_contract_addr), + ) + # Documented here: https://uniswap.org/docs/v2/smart-contracts/router02/ + self.router = _load_contract( + self.w3, + abi_name="uniswap-v2/router02", + address=self.router_address, + ) + elif self.version == 3: + # https://github.com/Uniswap/uniswap-v3-periphery/blob/main/deploys.md + factory_contract_address = _str_to_addr( + "0x1F98431c8aD98523631AE4a59f267346ea31F984" + ) + self.factory_contract = _load_contract( + self.w3, abi_name="uniswap-v3/factory", address=factory_contract_address + ) + quoter_addr = _str_to_addr("0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6") + self.router_address = _str_to_addr( + "0xE592427A0AEce92De3Edee1F18E0157C05861564" + ) + self.quoter = _load_contract( + self.w3, abi_name="uniswap-v3/quoter", address=quoter_addr + ) + self.router = _load_contract( + self.w3, abi_name="uniswap-v3/router", address=self.router_address + ) + self.positionManager_addr = _str_to_addr( + "0xC36442b4a4522E871399CD717aBDD847Ab11FE88" + ) + self.nonFungiblePositionManager = _load_contract( + self.w3, + abi_name="uniswap-v3/nonFungiblePositionManager", + address=self.positionManager_addr, + ) + if self.netname == "arbitrum": + multicall2_addr = _str_to_addr( + "0x50075F151ABC5B6B448b1272A0a1cFb5CFA25828" + ) + else: + multicall2_addr = _str_to_addr( + "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696" + ) + self.multicall2 = _load_contract( + self.w3, abi_name="uniswap-v3/multicall", address=multicall2_addr + ) + else: + raise ValueError( + f"Invalid version '{self.version}', only 1, 2 or 3 supported" + ) + + if hasattr(self, "factory_contract"): + logger.info(f"Using factory contract: {self.factory_contract}") + + # ------ Market -------------------------------------------------------------------- + + async def get_price_input( + self, + token0: AddressLike, # input token + token1: AddressLike, # output token + qty: int, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> int: + """Given `qty` amount of the input `token0`, returns the maximum output amount of output `token1`.""" + fee = validate_fee_tier(fee=fee, version=self.version) + + if is_same_address(token0, ETH_ADDRESS) and self.version == 2: + return await self._get_eth_token_input_price(token1, Wei(qty), fee) + elif is_same_address(token1, ETH_ADDRESS) and self.version == 2: + return await self._get_token_eth_input_price(token0, qty, fee) + else: + if is_same_address(token0, ETH_ADDRESS): + token0 = await self.get_weth_address() + if is_same_address(token1, ETH_ADDRESS): + token1 = await self.get_weth_address() + return await self._get_token_token_input_price( + token0, token1, qty, fee, route, fees + ) + + async def get_price_output( + self, + token0: AddressLike, + token1: AddressLike, + qty: int, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> int: + """Returns the minimum amount of `token0` required to buy `qty` amount of `token1`.""" + fee = validate_fee_tier(fee=fee, version=self.version) + + if is_same_address(token0, ETH_ADDRESS) and self.version == 2: + return await self._get_eth_token_output_price(token1, qty, fee) + elif is_same_address(token1, ETH_ADDRESS) and self.version == 2: + return await self._get_token_eth_output_price(token0, Wei(qty), fee) + else: + if is_same_address(token0, ETH_ADDRESS): + token0 = await self.get_weth_address() + if is_same_address(token1, ETH_ADDRESS): + token1 = await self.get_weth_address() + return await self._get_token_token_output_price( + token0, token1, qty, fee, route, fees + ) + + async def _get_eth_token_input_price( + self, + token: AddressLike, # output token + qty: Wei, + fee: int, + ) -> Wei: + """Public price (i.e. amount of output token received) for ETH to token trades with an exact input.""" + if self.version == 1: + ex = await self._exchange_contract(token) + price: Wei = await ex.functions.getEthToTokenInputPrice(qty).call() + elif self.version == 2: + price = ( + await self.router.functions.getAmountsOut( + qty, [await self.get_weth_address(), token] + ).call() + )[-1] + elif self.version == 3: + price = await self._get_token_token_input_price( + await self.get_weth_address(), token, qty, fee=fee + ) # type: ignore + else: + raise ValueError # pragma: no cover + return price + + async def _get_token_eth_input_price( + self, + token: AddressLike, # input token + qty: int, + fee: int, + ) -> int: + """Public price (i.e. amount of ETH received) for token to ETH trades with an exact input.""" + if self.version == 1: + ex = await self._exchange_contract(token) + price: int = await ex.functions.getTokenToEthInputPrice(qty).call() + elif self.version == 2: + price = ( + await self.router.functions.getAmountsOut( + qty, [token, await self.get_weth_address()] + ).call() + )[-1] + elif self.version == 3: + price = await self._get_token_token_input_price( + token, await self.get_weth_address(), qty, fee=fee + ) + else: + raise ValueError # pragma: no cover + return price + + async def _get_token_token_input_price( + self, + token0: AddressLike, # input token + token1: AddressLike, # output token + qty: int, + fee: int, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> int: + """ + Public price (i.e. amount of output token received) for token to token trades with an exact input. + + :param fee: (v3 only) The pool's fee in hundredths of a bip, i.e. 1e-6 (3000 is 0.3%) + """ + if route is None and self.version == 2: + # If one of the tokens are WETH, delegate to appropriate call. + # See: https://github.com/shanefontaine/uniswap-python/issues/22 + if is_same_address(token0, await self.get_weth_address()): + return int(await self._get_eth_token_input_price(token1, Wei(qty), fee)) + elif is_same_address(token1, await self.get_weth_address()): + return int(await self._get_token_eth_input_price(token0, qty, fee)) + + route = [token0, await self.get_weth_address(), token1] + logger.warning(f"No route specified, assuming route: {route}") + + if self.version == 2: + price: int = (await self.router.functions.getAmountsOut(qty, route).call())[ + -1 + ] + elif self.version == 3: + if route and fees: + # NOTE: to support custom routes we need to support the Path data encoding: https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/libraries/Path.sol + # result: tuple = self.quoter.functions.quoteExactInput(route, qty).call() + encoded_v3_path = self.encode_v3_path(route, fees) + price = await self.quoter.functions.quoteExactInput( + encoded_v3_path, qty + ).call() + else: + # FIXME: How to calculate this properly? See https://docs.uniswap.org/reference/libraries/SqrtPriceMath + sqrtPriceLimitX96 = 0 + price = await self.quoter.functions.quoteExactInputSingle( + token0, token1, fee, qty, sqrtPriceLimitX96 + ).call() + else: + raise ValueError("function not supported for this version of Uniswap") + return price + + async def _get_eth_token_output_price( + self, + token: AddressLike, # output token + qty: int, + fee: int | None = None, + ) -> Wei: + """Public price (i.e. amount of ETH needed) for ETH to token trades with an exact output.""" + fee = validate_fee_tier(fee=fee, version=self.version) + if self.version == 1: + ex = await self._exchange_contract(token) + price: Wei = await ex.functions.getEthToTokenOutputPrice(qty).call() + elif self.version == 2: + route = [await self.get_weth_address(), token] + price = (await self.router.functions.getAmountsIn(qty, route).call())[0] + elif self.version == 3: + price = Wei( + await self._get_token_token_output_price( + await self.get_weth_address(), token, qty, fee=fee + ) + ) + else: + raise ValueError # pragma: no cover + return price + + async def _get_token_eth_output_price( + self, + token: AddressLike, + qty: Wei, + fee: int | None = None, # input token + ) -> int: + """Public price (i.e. amount of input token needed) for token to ETH trades with an exact output.""" + fee = validate_fee_tier(fee=fee, version=self.version) + if self.version == 1: + ex = await self._exchange_contract(token) + price: int = await ex.functions.getTokenToEthOutputPrice(qty).call() + elif self.version == 2: + route = [token, await self.get_weth_address()] + price = (await self.router.functions.getAmountsIn(qty, route).call())[0] + elif self.version == 3: + price = await self._get_token_token_output_price( + token, await self.get_weth_address(), qty, fee=fee + ) + else: + raise ValueError # pragma: no cover + return price + + async def _get_token_token_output_price( + self, + token0: AddressLike, # input token + token1: AddressLike, # output token + qty: int, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> int: + """ + Public price (i.e. amount of input token needed) for token to token trades with an exact output. + + :param fee: (v3 only) The pool's fee in hundredths of a bip, i.e. 1e-6 (3000 is 0.3%) + """ + fee = validate_fee_tier(fee=fee, version=self.version) + if not route and self.version == 2: + # If one of the tokens are WETH, delegate to appropriate call. + # See: https://github.com/shanefontaine/uniswap-python/issues/22 + if is_same_address(token0, await self.get_weth_address()): + return int(await self._get_eth_token_output_price(token1, qty, fee)) + elif is_same_address(token1, await self.get_weth_address()): + return int( + await self._get_token_eth_output_price(token0, Wei(qty), fee) + ) + + route = [token0, await self.get_weth_address(), token1] + logger.warning(f"No route specified, assuming route: {route}") + + if self.version == 2: + price: int = (await self.router.functions.getAmountsIn(qty, route).call())[ + 0 + ] + elif self.version == 3: + if route and fees: + # NOTE: to support custom routes we need to support the Path data encoding: https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/libraries/Path.sol + # result: tuple = self.quoter.functions.quoteExactOutput(route, qty).call() + encoded_v3_path = self.encode_v3_path(route, fees, is_exact_out=True) + price = await self.quoter.functions.quoteExactOutput( + encoded_v3_path, qty + ).call() + else: + # FIXME: How to calculate this properly? + # - https://docs.uniswap.org/reference/libraries/SqrtPriceMath + # - https://github.com/Uniswap/uniswap-v3-sdk/blob/main/src/swapRouter.ts + sqrtPriceLimitX96 = 0 + price = await self.quoter.functions.quoteExactOutputSingle( + token0, token1, fee, qty, sqrtPriceLimitX96 + ).call() + else: + raise ValueError # pragma: no cover + return price + + # ------ Make Trade ---------------------------------------------------------------- + async def make_trade( + self, + input_token: AddressLike, + output_token: AddressLike, + qty: int | Wei, + recipient: AddressLike | None = None, + fee: int | None = None, + slippage: float | None = None, + fee_on_transfer: bool = False, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> HexBytes: + """Make a trade by defining the qty of the input token.""" + if not isinstance(qty, int): + raise TypeError("swapped quantity must be an integer") + + fee = validate_fee_tier(fee=fee, version=self.version) + + if slippage is None: + slippage = self.default_slippage + + if input_token == output_token: + raise ValueError + + if self.version != 3 and fees: + fees = None + + if input_token == ETH_ADDRESS and (route is None or fees is None): + return await self._eth_to_token_swap_input( + output_token, Wei(qty), recipient, fee, slippage, fee_on_transfer + ) + elif output_token == ETH_ADDRESS and (route is None or fees is None): + return await self._token_to_eth_swap_input( + input_token, qty, recipient, fee, slippage, fee_on_transfer + ) + else: + # if is_same_address(input_token, ETH_ADDRESS): + # input_token = await self.get_weth_address() + # if is_same_address(output_token, ETH_ADDRESS): + # output_token = await self.get_weth_address() + return await self._token_to_token_swap_input( + input_token, + output_token, + qty, + recipient, + fee, + slippage, + fee_on_transfer, + route, + fees, + ) + + async def make_trade_output( + self, + input_token: AddressLike, + output_token: AddressLike, + qty: int | Wei, + recipient: AddressLike | None = None, + fee: int | None = None, + slippage: float | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> HexBytes: + """Make a trade by defining the qty of the output token.""" + fee = validate_fee_tier(fee=fee, version=self.version) + + if slippage is None: + slippage = self.default_slippage + + if input_token == output_token: + raise ValueError + + if self.version != 3 and fees: + fees = None + + if input_token == ETH_ADDRESS and (route is None or fees is None): + balance = await self.get_eth_balance() + need = await self._get_eth_token_output_price(output_token, qty, fee) + if balance < need: + raise InsufficientBalance(balance, need) + return await self._eth_to_token_swap_output( + output_token, qty, recipient, fee, slippage + ) + elif output_token == ETH_ADDRESS and (route is None or fees is None): + return await self._token_to_eth_swap_output( + input_token, Wei(qty), recipient, fee, slippage + ) + else: + # if is_same_address(input_token, ETH_ADDRESS): + # input_token = await self.get_weth_address() + # if is_same_address(output_token, ETH_ADDRESS): + # output_token = await self.get_weth_address() + return await self._token_to_token_swap_output( + input_token, output_token, qty, recipient, fee, slippage, route, fees + ) + + async def _eth_to_token_swap_input( + self, + output_token: AddressLike, + qty: Wei, + recipient: AddressLike | None, + fee: int, + slippage: float, + fee_on_transfer: bool = False, + ) -> HexBytes: + """Convert ETH to tokens given an input amount.""" + if output_token == ETH_ADDRESS: + raise ValueError + + eth_balance = await self.get_eth_balance() + if qty > eth_balance: + raise InsufficientBalance(eth_balance, qty) + + if self.version == 1: + token_funcs = (await self._exchange_contract(output_token)).functions + tx_params = await self._get_tx_params(qty) + func_params: list[Any] = [qty, self._deadline()] + if not recipient: + function = token_funcs.ethToTokenSwapInput(*func_params) + else: + func_params.append(recipient) + function = token_funcs.ethToTokenTransferInput(*func_params) + return await self._build_and_send_tx(function, tx_params) + + elif self.version == 2: + if recipient is None: + recipient = self.address + amount_out_min = int( + (1 - slippage) + * await self._get_eth_token_input_price(output_token, qty, fee) + ) + if fee_on_transfer: + func = self.router.functions.swapExactETHForTokensSupportingFeeOnTransferTokens + else: + func = self.router.functions.swapExactETHForTokens + return await self._build_and_send_tx( + func( + amount_out_min, + [await self.get_weth_address(), output_token], + recipient, + self._deadline(), + ), + await self._get_tx_params(qty), + ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + if fee_on_transfer: + raise ValueError("fee on transfer not supported by Uniswap v3") + + min_tokens_bought = int( + (1 - slippage) + * await self._get_eth_token_input_price(output_token, qty, fee=fee) + ) + sqrtPriceLimitX96 = 0 + + return await self._build_and_send_tx( + self.router.functions.exactInputSingle( + { + "tokenIn": await self.get_weth_address(), + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + } + ), + await self._get_tx_params(value=qty), + ) + else: + raise ValueError # pragma: no cover + + async def _token_to_eth_swap_input( + self, + input_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + fee_on_transfer: bool = False, + ) -> HexBytes: + """Convert tokens to ETH given an input amount.""" + if input_token == ETH_ADDRESS: + raise ValueError + + # Balance check + input_balance = await self.get_token_balance(input_token) + if qty > input_balance: + raise InsufficientBalance(input_balance, qty) + + if self.version == 1: + token_funcs = (await self._exchange_contract(input_token)).functions + func_params: list[Any] = [qty, 1, self._deadline()] + if not recipient: + function = token_funcs.tokenToEthSwapInput(*func_params) + else: + func_params.append(recipient) + function = token_funcs.tokenToEthTransferInput(*func_params) + return await self._build_and_send_tx(function) + elif self.version == 2: + if recipient is None: + recipient = self.address + amount_out_min = int( + (1 - slippage) + * await self._get_token_eth_input_price(input_token, qty, fee) + ) + if fee_on_transfer: + func = self.router.functions.swapExactTokensForETHSupportingFeeOnTransferTokens + else: + func = self.router.functions.swapExactTokensForETH + return await self._build_and_send_tx( + func( + qty, + amount_out_min, + [input_token, await self.get_weth_address()], + recipient, + self._deadline(), + ), + ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + if fee_on_transfer: + raise ValueError("fee on transfer not supported by Uniswap v3") + + output_token = await self.get_weth_address() + min_tokens_bought = int( + (1 - slippage) + * await self._get_token_eth_input_price(input_token, qty, fee=fee) + ) + sqrtPriceLimitX96 = 0 + + swap_data = self.router.encode_abi( + "exactInputSingle", + args=[ + ( + input_token, + output_token, + fee, + ETH_ADDRESS, + self._deadline(), + qty, + min_tokens_bought, + sqrtPriceLimitX96, + ) + ], + ) + + unwrap_data = self.router.encode_abi( + "unwrapWETH9", args=[min_tokens_bought, recipient] + ) + + # Multicall + return await self._build_and_send_tx( + self.router.functions.multicall([swap_data, unwrap_data]), + await self._get_tx_params(), + ) + else: + raise ValueError # pragma: no cover + + async def _token_to_token_swap_input( + self, + input_token: AddressLike, + output_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + fee_on_transfer: bool = False, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> HexBytes: + """Convert tokens to tokens given an input amount.""" + # Balance check + input_balance = await self.get_token_balance(input_token) + if qty > input_balance: + raise InsufficientBalance(input_balance, qty) + + if recipient is None: + recipient = self.address + + if (input_token == ETH_ADDRESS or output_token == ETH_ADDRESS) and ( + route is None or fees is None + ): + raise ValueError + + if self.version == 1: + token_funcs = (await self._exchange_contract(input_token)).functions + # TODO: This might not be correct + min_tokens_bought, min_eth_bought = await self._calculate_max_output_token( + input_token, qty, output_token + ) + func_params = [ + qty, + min_tokens_bought, + min_eth_bought, + self._deadline(), + output_token, + ] + if not recipient: + function = token_funcs.tokenToTokenSwapInput(*func_params) + else: + func_params.insert(len(func_params) - 1, recipient) + function = token_funcs.tokenToTokenTransferInput(*func_params) + return await self._build_and_send_tx(function) + elif self.version == 2: + if fee_on_transfer: + func = self.router.functions.swapExactTokensForTokensSupportingFeeOnTransferTokens + else: + func = self.router.functions.swapExactTokensForTokens + if route is None: + weth_address = await self.get_weth_address() + if is_same_address(input_token, weth_address) or is_same_address( + output_token, weth_address + ): + path = [input_token, output_token] + else: + path = [input_token, weth_address, output_token] + else: + path = route + min_tokens_bought = int( + (1 - slippage) + * await self._get_token_token_input_price( + input_token, output_token, qty, fee=fee, route=path, fees=fees + ) + ) + return await self._build_and_send_tx( + func( + qty, + min_tokens_bought, + path, + recipient, + self._deadline(), + ), + ) + elif self.version == 3: + if fee_on_transfer: + raise ValueError("fee on transfer not supported by Uniswap v3") + + min_tokens_bought = int( + (1 - slippage) + * await self._get_token_token_input_price( + input_token, output_token, qty, fee=fee, route=route, fees=fees + ) + ) + sqrtPriceLimitX96 = 0 + if route and fees: + encoded_v3_path = self.encode_v3_path(route, fees) + return await self._build_and_send_tx( + self.router.functions.exactInput( + { + "path": encoded_v3_path, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + } + ), + await self._get_tx_params(), + ) + else: + return await self._build_and_send_tx( + self.router.functions.exactInputSingle( + { + "tokenIn": input_token, + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + } + ), + await self._get_tx_params(), + ) + else: + raise ValueError # pragma: no cover + + async def _eth_to_token_swap_output( + self, + output_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + ) -> HexBytes: + """Convert ETH to tokens given an output amount.""" + if output_token == ETH_ADDRESS: + raise ValueError + + # Balance check + eth_balance = await self.get_eth_balance() + cost = await self._get_eth_token_output_price(output_token, qty, fee) + amount_in_max = Wei(int((1 + slippage) * cost)) + + # We check balance against amount_in_max rather than cost to be conservative + if amount_in_max > eth_balance: + raise InsufficientBalance(eth_balance, amount_in_max) + + if self.version == 1: + token_funcs = (await self._exchange_contract(output_token)).functions + eth_qty = await self._get_eth_token_output_price(output_token, qty) + tx_params = await self._get_tx_params(eth_qty) + func_params: list[Any] = [qty, self._deadline()] + if not recipient: + function = token_funcs.ethToTokenSwapOutput(*func_params) + else: + func_params.append(recipient) + function = token_funcs.ethToTokenTransferOutput(*func_params) + return await self._build_and_send_tx(function, tx_params) + elif self.version == 2: + if recipient is None: + recipient = self.address + eth_qty = int( + (1 + slippage) + * await self._get_eth_token_output_price(output_token, qty, fee) + ) # type: ignore + return await self._build_and_send_tx( + self.router.functions.swapETHForExactTokens( + qty, + [await self.get_weth_address(), output_token], + recipient, + self._deadline(), + ), + await self._get_tx_params(eth_qty), + ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + sqrtPriceLimitX96 = 0 + + swap_data = self.router.encode_abi( + "exactOutputSingle", + args=[ + ( + await self.get_weth_address(), + output_token, + fee, + recipient, + self._deadline(), + qty, + amount_in_max, + sqrtPriceLimitX96, + ) + ], + ) + + refund_data = self.router.encode_abi("refundETH", args=None) + + # Multicall + return await self._build_and_send_tx( + self.router.functions.multicall([swap_data, refund_data]), + await self._get_tx_params(value=amount_in_max), + ) + else: + raise ValueError + + async def _token_to_eth_swap_output( + self, + input_token: AddressLike, + qty: Wei, + recipient: AddressLike | None, + fee: int, + slippage: float, + ) -> HexBytes: + """Convert tokens to ETH given an output amount.""" + if input_token == ETH_ADDRESS: + raise ValueError + + # Balance check + input_balance = await self.get_token_balance(input_token) + cost = await self._get_token_eth_output_price(input_token, qty, fee) + amount_in_max = int((1 + slippage) * cost) + + # We check balance against amount_in_max rather than cost to be conservative + if amount_in_max > input_balance: + raise InsufficientBalance(input_balance, amount_in_max) + + if self.version == 1: + # From https://uniswap.org/docs/v1/frontend-integration/trade-tokens/ + # Is all this really necessary? Can't we just use `cost` for max_tokens? + outputAmount = qty + inputReserve = await self.get_ex_token_balance(input_token) + outputReserve = await self.get_ex_eth_balance(input_token) + + numerator = outputAmount * inputReserve * 1000 + denominator = (outputReserve - outputAmount) * 997 + inputAmount = numerator / denominator + 1 + + max_tokens = int((1 + slippage) * inputAmount) + + ex = await self._exchange_contract(input_token) + func_params: list[Any] = [qty, max_tokens, self._deadline()] + if not recipient: + function = ex.functions.tokenToEthSwapOutput(*func_params) + else: + func_params.append(recipient) + function = ex.functions.tokenToEthTransferOutput(*func_params) + return await self._build_and_send_tx(function) + elif self.version == 2: + if recipient is None: + recipient = self.address + + max_tokens = int((1 + slippage) * cost) + return await self._build_and_send_tx( + self.router.functions.swapTokensForExactETH( + qty, + max_tokens, + [input_token, await self.get_weth_address()], + recipient, + self._deadline(), + ), + ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + sqrtPriceLimitX96 = 0 + + swap_data = self.router.encode_abi( + "exactOutputSingle", + args=[ + ( + input_token, + await self.get_weth_address(), + fee, + ETH_ADDRESS, + self._deadline(), + qty, + amount_in_max, + sqrtPriceLimitX96, + ) + ], + ) + + unwrap_data = self.router.encode_abi("unwrapWETH9", args=[qty, recipient]) + + # Multicall + return await self._build_and_send_tx( + self.router.functions.multicall([swap_data, unwrap_data]), + await self._get_tx_params(), + ) + else: + raise ValueError + + async def _token_to_token_swap_output( + self, + input_token: AddressLike, + output_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> HexBytes: + """Convert tokens to tokens given an output amount. + + :param fee: TODO + """ + if (input_token == ETH_ADDRESS or output_token == ETH_ADDRESS) and ( + route is None or fees is None + ): + raise ValueError + + # Balance check + input_balance = await self.get_token_balance(input_token) + cost = await self._get_token_token_output_price( + input_token, output_token, qty, fee, route, fees + ) + amount_in_max = int((1 + slippage) * cost) + if ( + amount_in_max > input_balance + ): # We check balance against amount_in_max rather than cost to be conservative + raise InsufficientBalance(input_balance, amount_in_max) + + if self.version == 1: + token_funcs = (await self._exchange_contract(input_token)).functions + max_tokens_sold, max_eth_sold = await self._calculate_max_input_token( + input_token, qty, output_token + ) + tx_params = await self._get_tx_params() + func_params = [ + qty, + max_tokens_sold, + max_eth_sold, + self._deadline(), + output_token, + ] + if not recipient: + function = token_funcs.tokenToTokenSwapOutput(*func_params) + else: + func_params.insert(len(func_params) - 1, recipient) + function = token_funcs.tokenToTokenTransferOutput(*func_params) + return await self._build_and_send_tx(function, tx_params) + elif self.version == 2: + if recipient is None: + recipient = self.address + if route is None: + weth = await self.get_weth_address() + path = ( + [input_token, output_token] + if is_same_address(input_token, weth) + or is_same_address(output_token, weth) + else [input_token, weth, output_token] + ) + else: + path = route + cost = await self._get_token_token_output_price( + input_token, output_token, qty, fee=fee, route=path, fees=fees + ) + amount_in_max = int((1 + slippage) * cost) + return await self._build_and_send_tx( + self.router.functions.swapTokensForExactTokens( + qty, + amount_in_max, + path, + recipient, + self._deadline(), + ), + ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + sqrtPriceLimitX96 = 0 + + if route and fees: + encoded_v3_path = self.encode_v3_path(route, fees, is_exact_out=True) + return await self._build_and_send_tx( + self.router.functions.exactOutput( + { + "path": encoded_v3_path, + "recipient": recipient, + "deadline": self._deadline(), + "amountOut": qty, + "amountInMaximum": amount_in_max, + } + ), + await self._get_tx_params(), + ) + else: + return await self._build_and_send_tx( + self.router.functions.exactOutputSingle( + { + "tokenIn": input_token, + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountOut": qty, + "amountInMaximum": amount_in_max, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + }, + ), + await self._get_tx_params(), + ) + else: + raise ValueError + + # ------ Wallet balance ------------------------------------------------------------ + async def get_eth_balance(self) -> Wei: + """Get the balance of ETH for your address.""" + return await self.w3.eth.get_balance(self.address) + + async def get_token_balance(self, token: AddressLike) -> int: + """Get the balance of a token for your address.""" + _validate_address(token) + if _addr_to_str(token) == ETH_ADDRESS: + return await self.get_eth_balance() + erc20 = _load_contract_erc20(self.w3, token) + balance: int = await erc20.functions.balanceOf(self.address).call() + return balance + + # ------ ERC20 Pool ---------------------------------------------------------------- + async def get_ex_eth_balance(self, token: AddressLike) -> int: + """Get the balance of ETH in an exchange contract.""" + ex_addr: AddressLike = await self._exchange_address_from_token(token) + return await self.w3.eth.get_balance(ex_addr) + + async def get_ex_token_balance(self, token: AddressLike) -> int: + """Get the balance of a token in an exchange contract.""" + erc20 = _load_contract_erc20(self.w3, token) + balance: int = await erc20.functions.balanceOf( + await self._exchange_address_from_token(token) + ).call() + return balance + + # TODO: ADD TOTAL SUPPLY + async def get_exchange_rate(self, token: AddressLike) -> float: + """Get the current ETH/token exchange rate of the token.""" + eth_reserve = await self.get_ex_eth_balance(token) + token_reserve = await self.get_ex_token_balance(token) + return float(token_reserve / eth_reserve) + + # ------ Liquidity ----------------------------------------------------------------- + async def add_liquidity( + self, token: AddressLike, max_eth: Wei, min_liquidity: int = 1 + ) -> HexBytes: + """Add liquidity to the pool.""" + tx_params = await self._get_tx_params(max_eth) + # Add 1 to avoid rounding errors, per + # https://hackmd.io/hthz9hXKQmSyXfMbPsut1g#Add-Liquidity-Calculations + max_token = int(max_eth * await self.get_exchange_rate(token)) + 10 + func_params = [min_liquidity, max_token, self._deadline()] + function = (await self._exchange_contract(token)).functions.addLiquidity( + *func_params + ) + return await self._build_and_send_tx(function, tx_params) + + async def remove_liquidity(self, token: str, max_token: int) -> HexBytes: + """Remove liquidity from the pool.""" + func_params = [int(max_token), 1, 1, self._deadline()] + function = ( + await self._exchange_contract(self.w3.to_checksum_address(token)) + ).functions.removeLiquidity(*func_params) + return await self._build_and_send_tx(function) + + async def mint_liquidity( + self, + pool: AsyncContract, + amount_0: int, + amount_1: int, + tick_lower: int, + tick_upper: int, + deadline: int = 2**64, + ) -> TxReceipt: + """ + add liquidity to pool and mint position nft + """ + + token_0 = await pool.functions.token0().call() + token_1 = await pool.functions.token1().call() + token_0_instance = _load_contract(self.w3, abi_name="erc20", address=token_0) + token_1_instance = _load_contract(self.w3, abi_name="erc20", address=token_1) + + balance_0 = await self.get_token_balance(token_0) + balance_1 = await self.get_token_balance(token_1) + + assert balance_0 > amount_0, f"Have {balance_0}, need {amount_0}: {token_0}" + assert balance_1 > amount_1, f"Have {balance_1}, need {amount_1}: {token_1}" + + fee = await pool.functions.fee().call() + tick_lower = nearest_tick(tick_lower, fee) + tick_upper = nearest_tick(tick_upper, fee) + assert tick_lower < tick_upper, "Invalid tick range" + + *_, isInit = await pool.functions.slot0().call() + # If pool is not initialized, init pool w/ sqrt_price_x96 encoded from amount_0 & amount_1 + if isInit is False: + sqrt_pricex96 = encode_sqrt_ratioX96(amount_0, amount_1) + await pool.functions.initialize(sqrt_pricex96).transact( + {"from": _addr_to_str(self.address)} + ) + + nft_manager = self.nonFungiblePositionManager + await token_0_instance.functions.approve( + nft_manager.address, amount_0 + ).transact({"from": _addr_to_str(self.address)}) + await token_1_instance.functions.approve( + nft_manager.address, amount_1 + ).transact({"from": _addr_to_str(self.address)}) + + # TODO: add slippage param + tx_hash = await nft_manager.functions.mint( + ( + token_0, + token_1, + fee, + tick_lower, + tick_upper, + amount_0, + amount_1, + 0, + 0, + self.address, + deadline, + ) + ).transact({"from": _addr_to_str(self.address)}) + receipt = await self.w3.eth.wait_for_transaction_receipt(tx_hash) + return receipt + + # TODO: should this be multiple functions? + async def close_position( + self, + tokenId: int, + amount0Min: int = 0, + amount1Min: int = 0, + deadline: int | None = None, + ) -> TxReceipt: + """ + remove all liquidity from the position associated w/ tokenId, collect fees, and burn token. + """ + position = await self.nonFungiblePositionManager.functions.positions( + tokenId + ).call() + + if deadline is None: + deadline = self._deadline() + + # If collecting fees in ETH, fees must be precomputed to protect against reentrancy + # source: https://docs.uniswap.org/sdk/guides/liquidity/removing + + if position[2] == WETH9_ADDRESS or position[3] == WETH9_ADDRESS: + ( + amount0Min, + amount1Min, + ) = await self.nonFungiblePositionManager.functions.collect( + (tokenId, _addr_to_str(self.address), MAX_UINT_128, MAX_UINT_128) + ).call() + + tx_remove_liquidity = await ( + self.nonFungiblePositionManager.functions.decreaseLiquidity( + (tokenId, position[7], amount0Min, amount1Min, deadline) + ).transact({"from": _addr_to_str(self.address)}) + ) + await self.w3.eth.wait_for_transaction_receipt(tx_remove_liquidity) + + tx_collect_fees = await self.nonFungiblePositionManager.functions.collect( + (tokenId, _addr_to_str(self.address), MAX_UINT_128, MAX_UINT_128) + ).transact({"from": _addr_to_str(self.address)}) + await self.w3.eth.wait_for_transaction_receipt(tx_collect_fees) + + tx_burn = await self.nonFungiblePositionManager.functions.burn( + tokenId + ).transact({"from": _addr_to_str(self.address)}) + receipt = await self.w3.eth.wait_for_transaction_receipt(tx_burn) + + return receipt + + # Below two functions derived from: https://stackoverflow.com/questions/71814845/how-to-calculate-uniswap-v3-pools-total-value-locked-tvl-on-chain + def get_token0_in_pool( + self, + liquidity: float, + sqrtPrice: float, + sqrtPriceLow: float, + sqrtPriceHigh: float, + ) -> float: + sqrtPrice = max(min(sqrtPrice, sqrtPriceHigh), sqrtPriceLow) + return liquidity * (sqrtPriceHigh - sqrtPrice) / (sqrtPrice * sqrtPriceHigh) + + def get_token1_in_pool( + self, + liquidity: float, + sqrtPrice: float, + sqrtPriceLow: float, + sqrtPriceHigh: float, + ) -> float: + sqrtPrice = max(min(sqrtPrice, sqrtPriceHigh), sqrtPriceLow) + return liquidity * (sqrtPrice - sqrtPriceLow) + + # Find maximum tick of the word at the largest index (wordPos) in the tickBitmap that contains an initialized tick + def get_max_tick_from_wordpos( + self, wordPos: int, bitmap: str, tick_spacing: int, fee: int + ) -> int: + compressed_tick = wordPos << 8 + _tick = compressed_tick * tick_spacing + min_tick_in_word = nearest_tick(_tick, fee) + max_tick_in_word = min_tick_in_word + (len(bitmap) * tick_spacing) + return max_tick_in_word + + # Find minimum tick of word at the smallest index (wordPos) in the tickBitmap that contains an initialized tick + def get_min_tick_from_wordpos( + self, wordPos: int, tick_spacing: int, fee: int + ) -> int: + compressed_tick = wordPos << 8 + _tick = compressed_tick * tick_spacing + min_tick_in_word = nearest_tick(_tick, fee) + return min_tick_in_word + + # Find min or max tick in initialized tick range using the tickBitmap + async def find_tick_from_bitmap( + self, + bitmap_spacing: tuple[int, int], + pool: AsyncContract, + tick_spacing: int, + fee: int, + left: bool = True, + ) -> int | bool: + # searching to the left (finding max tick) + if left: + min_wordPos = bitmap_spacing[1] + max_wordPos = bitmap_spacing[0] + step = -1 + # searching to the right (finding min tick) + else: + min_wordPos = bitmap_spacing[0] + max_wordPos = bitmap_spacing[1] + step = 1 + + # Some fun tickBitmap hacks below. + # Iterate thru each possible wordPos (based on tick_spacing), get the bitmap "word" (basically a sub-array of the full bitmap), + # check if there is an initialized tick, derive largest (or smallest) tick in this word + # + # Since wordPos (int16 index of tickBitmap mapping) are calculated by (tick/tickspacing) >> 8, deriving tick from wordPos + # is done by (wordPos << 8)*tickSpacing. This however does not find the precise tick (only a possible tick that could map to that bitmap sub-array, or word), + # thus we must calculate the nearest viable tick depending on the tick_spacing of the pool using nearest_tick(). + # If searching for the maximum tick, we must then add-back len(bitmap)*tick_spacing as each bit in the bitmap should correspond to a tick. + + for wordPos in range(min_wordPos, max_wordPos, step): + word = await pool.functions.tickBitmap(wordPos).call() + bitmap = bin(word) + for bit in bitmap[3:]: + if int(bit) == 1: + if left: + _max_tick = self.get_max_tick_from_wordpos( + wordPos, bitmap, tick_spacing, fee + ) + return _max_tick + else: + _min_tick = self.get_min_tick_from_wordpos( + wordPos, tick_spacing, fee + ) + return _min_tick + return False + + async def get_tvl_in_pool(self, pool: AsyncContract) -> tuple[float, float]: + """ + Iterate through each tick in a pool and calculate the TVL on-chain + + Note: the output of this function may differ from what is returned by the + UniswapV3 subgraph api (https://github.com/Uniswap/v3-subgraph/issues/74) + + Params + ------ + pool: Contract + pool contract instance to find TVL + """ + pool_tick_output_types = ( + "uint128", + "int128", + "uint256", + "uint256", + "int56", + "uint160", + "uint32", + "bool", + ) + + pool_immutables = await self.get_pool_immutables(pool) + pool_state = await self.get_pool_state(pool) + fee = pool_immutables["fee"] + sqrtPrice = pool_state["sqrtPriceX96"] / (1 << 96) + + token0_liquidity = 0.0 + token1_liquidity = 0.0 + liquidity_total = 0.0 + + TICK_SPACING = _tick_spacing[fee] + BITMAP_SPACING = _tick_bitmap_range[fee] + + _max_tick = await self.find_tick_from_bitmap( + BITMAP_SPACING, pool, TICK_SPACING, fee, True + ) + _min_tick = await self.find_tick_from_bitmap( + BITMAP_SPACING, pool, TICK_SPACING, fee, False + ) + assert _max_tick is not False, "Error finding max tick" + assert _min_tick is not False, "Error finding min tick" + + Batch = namedtuple("Batch", "ticks batchResults") + ticks = [] + # Batching pool.functions.tick() calls as these are the major bottleneck to performance + for batch in list(chunks(range(_min_tick, _max_tick, TICK_SPACING), 100)): + _batch = [] + _ticks = [] + for tick in batch: + _batch.append( + ( + pool.address, + HexBytes(pool.functions.ticks(tick)._encode_transaction_data()), + ) + ) + _ticks.append(tick) + ticks.append( + Batch(_ticks, await self.multicall(_batch, pool_tick_output_types)) + ) + + for tickBatch in ticks: + tick_arr = tickBatch.ticks + for i in range(len(tick_arr)): + tick = tick_arr[i] + tickData = tickBatch.batchResults[i] + # source: https://stackoverflow.com/questions/71814845/how-to-calculate-uniswap-v3-pools-total-value-locked-tvl-on-chain + liquidityNet = tickData[1] + liquidity_total += liquidityNet + sqrtPriceLow = 1.0001 ** (tick // 2) + sqrtPriceHigh = 1.0001 ** ((tick + TICK_SPACING) // 2) + token0_liquidity += self.get_token0_in_pool( + liquidity_total, sqrtPrice, sqrtPriceLow, sqrtPriceHigh + ) + token1_liquidity += self.get_token1_in_pool( + liquidity_total, sqrtPrice, sqrtPriceLow, sqrtPriceHigh + ) + + # Correcting for each token's respective decimals + token0_decimals = await ( + _load_contract_erc20(self.w3, pool_immutables["token0"]) + .functions.decimals() + .call() + ) + token1_decimals = await ( + _load_contract_erc20(self.w3, pool_immutables["token1"]) + .functions.decimals() + .call() + ) + token0_liquidity = token0_liquidity // (10**token0_decimals) + token1_liquidity = token1_liquidity // (10**token1_decimals) + return (token0_liquidity, token1_liquidity) + + # ------ Approval Utils ------------------------------------------------------------ + async def approve( + self, token: AddressLike, max_approval: int | None = None + ) -> TxReceipt: + """Give an exchange/router max approval of a token.""" + max_approval = self.max_approval_int if not max_approval else max_approval + contract_addr = ( + self._exchange_address_from_token(token) + if self.version == 1 + else self.router_address + ) + function = _load_contract_erc20(self.w3, token).functions.approve( + contract_addr, max_approval + ) + logger.warning(f"Approving {_addr_to_str(token)}...") + tx = await self._build_and_send_tx(function) + receipt = await self.w3.eth.wait_for_transaction_receipt(tx, timeout=6000) + + # Add extra sleep to let tx propagate correctly + await asyncio.sleep(1) + return receipt + + async def _is_approved(self, token: AddressLike) -> bool: + """Check to see if the exchange and token is approved.""" + _validate_address(token) + if self.version == 1: + contract_addr = await self._exchange_address_from_token(token) + elif self.version in [2, 3]: + contract_addr = self.router_address + else: + raise ValueError + amount = await ( + _load_contract_erc20(self.w3, token) + .functions.allowance(self.address, contract_addr) + .call() + ) + return bool(amount >= self.max_approval_check_int) + + # ------ Tx Utils ------------------------------------------------------------------ + def _deadline(self) -> int: + """Get a predefined deadline. 10min by default (same as the Uniswap SDK).""" + return int(time.time()) + 10 * 60 + + async def _build_and_send_tx( + self, + function: AsyncContractFunction, + tx_params: TxParams | None = None, + ) -> HexBytes: + """Build and send a transaction.""" + if not tx_params: + tx_params = await self._get_tx_params() + + # Pre-populate gas BEFORE build_transaction to prevent web3 from calling + # eth_estimateGas internally. web3 calls eth_estimateGas during + # build_transaction when no gas is provided, which fails for contracts + # with computed jumps (e.g. Vyper v1 exchange contracts) under Anvil's + # strict EVM. use_estimate_gas=True for networks like Arbitrum where 500k + # is not a safe default. + if "gas" not in tx_params and not self.use_estimate_gas: + tx_params["gas"] = Wei(500_000) + + transaction = await function.build_transaction(tx_params) + + if "gas" not in tx_params: + # use_estimate_gas=True: run explicit estimate with 20% margin + # The Uniswap V3 UI uses 20% margin for transactions + transaction["gas"] = Wei( + int(await self.w3.eth.estimate_gas(transaction) * 1.2) + ) + + signed_txn = self.w3.eth.account.sign_transaction( + transaction, private_key=self.private_key + ) + # TODO: This needs to get more complicated if we want to support replacing a transaction + # FIXME: This does not play nice if transactions are sent from other places using the same wallet. + try: + return await self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) + finally: + logger.debug(f"nonce: {tx_params['nonce']}") + self.last_nonce = Nonce(tx_params["nonce"] + 1) + + async def _get_tx_params( + self, value: Wei = Wei(0), gas: Wei | None = None + ) -> TxParams: + """Get generic transaction parameters.""" + params: TxParams = { + "from": _addr_to_str(self.address), + "value": value, + "nonce": max( + self.last_nonce, await self.w3.eth.get_transaction_count(self.address) + ), + } + + if gas: + params["gas"] = gas + + return params + + # ------ Price Calculation Utils --------------------------------------------------- + async def _calculate_max_input_token( + self, input_token: AddressLike, qty: int, output_token: AddressLike + ) -> tuple[int, int]: + """ + For buy orders (exact output), the cost (input) is calculated. + Calculate the max input and max eth sold for a token to token output swap. + Equation from: + - https://hackmd.io/hthz9hXKQmSyXfMbPsut1g + - https://uniswap.org/docs/v1/frontend-integration/trade-tokens/ + """ + # Buy TokenB with ETH + output_amount_b = qty + input_reserve_b = await self.get_ex_eth_balance(output_token) + output_reserve_b = await self.get_ex_token_balance(output_token) + + # Cost + numerator_b = output_amount_b * input_reserve_b * 1000 + denominator_b = (output_reserve_b - output_amount_b) * 997 + input_amount_b = numerator_b / denominator_b + 1 + + # Buy ETH with TokenA + output_amount_a = input_amount_b + input_reserve_a = await self.get_ex_token_balance(input_token) + output_reserve_a = await self.get_ex_eth_balance(input_token) + + # Cost + numerator_a = output_amount_a * input_reserve_a * 1000 + denominator_a = (output_reserve_a - output_amount_a) * 997 + input_amount_a = numerator_a / denominator_a - 1 + + return int(input_amount_a), int(1.2 * input_amount_b) + + async def _calculate_max_output_token( + self, output_token: AddressLike, qty: int, input_token: AddressLike + ) -> tuple[int, int]: + """ + For sell orders (exact input), the amount bought (output) is calculated. + Similar to _calculate_max_input_token, but for an exact input swap. + """ + # TokenA (ERC20) to ETH conversion + inputAmountA = qty + inputReserveA = await self.get_ex_token_balance(input_token) + outputReserveA = await self.get_ex_eth_balance(input_token) + + # Cost + numeratorA = inputAmountA * outputReserveA * 997 + denominatorA = inputReserveA * 1000 + inputAmountA * 997 + outputAmountA = numeratorA / denominatorA + + # ETH to TokenB conversion + inputAmountB = outputAmountA + inputReserveB = await self.get_ex_token_balance(output_token) + outputReserveB = await self.get_ex_eth_balance(output_token) + + # Cost + numeratorB = inputAmountB * outputReserveB * 997 + denominatorB = inputReserveB * 1000 + inputAmountB * 997 + outputAmountB = numeratorB / denominatorB + + return int(outputAmountB), int(1.2 * outputAmountA) + + # ------ Helpers ------------------------------------------------------------ + + # Batch contract function calls to speed up large on-chain data queries + async def multicall( + self, + encoded_functions: Sequence[tuple[ChecksumAddress, bytes]], + output_types: Sequence[str], + ) -> list[Any]: + """ + Calls aggregate() on Uniswap Multicall2 contract + + Params + ------ + encoded_functions : Sequence[Tuple[ChecksumAddress, bytes]] + array of tuples containing address of contract and byte-encoded transaction data + + output_types: Sequence[str] + array of solidity output types for decoding (e.g. uint256, bool, etc.) + + returns decoded results + """ + params = [ + {"target": target, "callData": callData} + for target, callData in encoded_functions + ] + _, results = await self.multicall2.functions.aggregate(params).call( + block_identifier="latest" + ) + decoded_results = [ + self.w3.codec.decode(output_types, multicall_result) + for multicall_result in results + ] + normalized_results = [ + map_abi_data(BASE_RETURN_NORMALIZERS, output_types, decoded_result) + for decoded_result in decoded_results + ] + return normalized_results + + async def get_token( + self, address: AddressLike, abi_name: str = "erc20" + ) -> ERC20Token: + """ + Retrieves metadata from the ERC20 contract of a given token, like its name, symbol, and decimals. + """ + # FIXME: This function should always return the same output for the same input + # and would therefore benefit from caching + if address == "0x0000000000000000000000000000000000000000": + # This isn't exactly right, but for all intents and purposes, + # ETH is treated as a ERC20 by Uniswap. + return ERC20Token( + address=address, + name="ETH", + symbol="ETH", + decimals=18, + ) + token_contract = _load_contract(self.w3, abi_name, address=address) + try: + _name = await token_contract.functions.name().call() + _symbol = await token_contract.functions.symbol().call() + decimals = await token_contract.functions.decimals().call() + except ValueError as e: + logger.warning( + f"Exception occurred while trying to get token {_addr_to_str(address)}: {e}" + ) + raise InvalidToken(address) + try: + name = _name.decode() + except (ValueError, AttributeError): + name = _name + try: + symbol = _symbol.decode() + except (ValueError, AttributeError): + symbol = _symbol + return ERC20Token(symbol, address, name, decimals) + + async def get_weth_address(self) -> ChecksumAddress: + """Retrieves the WETH address from the contracts (which may vary between chains).""" + if self.version == 1: + address: ChecksumAddress = self.w3.to_checksum_address(WETH9_ADDRESS) + elif self.version == 2: + # Contract calls should always return checksummed addresses + address = await self.router.functions.WETH().call() + elif self.version == 3: + address = await self.router.functions.WETH9().call() + else: + raise ValueError # pragma: no cover + + # Mainnet WETH9 address + # WETH9_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + # assert address == WETH9_ADDRESS, "WETH address mismatch" + + return address + + async def get_pool_instance( + self, token_0: AddressLike, token_1: AddressLike, fee: int = 3_000 + ) -> AsyncContract: + """ + Returns an instance of a pool contract for a given token pair and fee. + Requires pair [token_in, token_out, fee] has a direct pool. + Will return 0x0 address if pool does not exist. + """ + + assert token_0 != token_1, "Token addresses cannot be the same" + fee = validate_fee_tier(fee=fee, version=self.version) + + pool_address = await self.factory_contract.functions.getPool( + token_0, token_1, fee + ).call() + assert pool_address != ETH_ADDRESS, "0 address returned. Pool does not exist" + pool_instance = _load_contract( + self.w3, abi_name="uniswap-v3/pool", address=pool_address + ) + + return pool_instance + + async def create_pool_instance( + self, token_0: AddressLike, token_1: AddressLike, fee: int = 3_000 + ) -> AsyncContract: + """ + Creates and returns UniswapV3 Pool instance. Requires that fee is valid and no similar pool already exists. + """ + address = _addr_to_str(self.address) + assert token_0 != token_1, "Token addresses cannot be the same" + fee = validate_fee_tier(fee=fee, version=self.version) + + tx = await self.factory_contract.functions.createPool( + token_0, token_1, fee + ).transact({"from": address}) + receipt = await self.w3.eth.wait_for_transaction_receipt(tx) + + event_logs = await self.factory_contract.events.PoolCreated().process_receipt( + receipt + ) + pool_address = event_logs[0]["args"]["pool"] + pool_instance = _load_contract( + self.w3, abi_name="uniswap-v3/pool", address=pool_address + ) + + return pool_instance + + async def get_pool_immutables(self, pool: AsyncContract) -> dict: + """ + Fetch on-chain pool data. + """ + pool_immutables = { + "factory": await pool.functions.factory().call(), + "token0": await pool.functions.token0().call(), + "token1": await pool.functions.token1().call(), + "fee": await pool.functions.fee().call(), + "tickSpacing": await pool.functions.tickSpacing().call(), + "maxLiquidityPerTick": await pool.functions.maxLiquidityPerTick().call(), + } + + return pool_immutables + + async def get_pool_state(self, pool: AsyncContract) -> dict: + """ + Fetch on-chain pool state. + """ + liquidity = await pool.functions.liquidity().call() + slot = await pool.functions.slot0().call() + pool_state = { + "liquidity": liquidity, + "sqrtPriceX96": slot[0], + "tick": slot[1], + "observationIndex": slot[2], + "observationCardinality": slot[3], + "observationCardinalityNext": slot[4], + "feeProtocol": slot[5], + "unlocked": slot[6], + } + + return pool_state + + async def get_liquidity_positions(self) -> list[int]: + """ + Enumerates liquidity position tokens owned by address. + Returns array of token IDs. + """ + positions: list[int] = [] + number_of_positions = await self.nonFungiblePositionManager.functions.balanceOf( + _addr_to_str(self.address) + ).call() + if number_of_positions > 0: + for idx in range(number_of_positions): + position = ( + await self.nonFungiblePositionManager.functions.tokenOfOwnerByIndex( + _addr_to_str(self.address), idx + ).call() + ) + positions.append(position) + return positions + + # FIXME: mint call reverting - likely to do w/ passing struct args to contract function call + # FIXME: mint call reverting - likely due to handling of token amounts + + async def mint_position( + self, pool: AsyncContract, amount0: int, amount1: int + ) -> HexBytes: + pool_immutables = await self.get_pool_immutables(pool) + + token0 = pool_immutables["token0"] + token1 = pool_immutables["token1"] + fee = pool_immutables["fee"] + + positionManager = self.nonFungiblePositionManager + + approve0 = _load_contract_erc20(self.w3, token0).functions.approve( + self.positionManager_addr, amount0 + ) + logger.warning(f"Approving {_addr_to_str(token0)}...") + tx0 = await self._build_and_send_tx(approve0) + await self.w3.eth.wait_for_transaction_receipt(tx0, timeout=6000) + + approve1 = _load_contract_erc20(self.w3, token1).functions.approve( + self.positionManager_addr, amount1 * 1000 + ) + logger.warning(f"Approving {_addr_to_str(token1)}...") + tx1 = await self._build_and_send_tx(approve1) + await self.w3.eth.wait_for_transaction_receipt(tx1, timeout=6000) + + # tx_mint = pool.functions.mint(self.address, MIN_TICK, MAX_TICK, amount0,'').transact(); + + position = positionManager.encode_abi( + "mint", + args=[ + { + "token0": token0, + "token1": token1, + "fee": fee, + "tickLower": MIN_TICK, + "tickUpper": MAX_TICK, + "amount0Desired": amount0, + "amount1Desired": amount1, + "amount0Min": 0, + "amount1Min": 0, + "recipient": _addr_to_str(self.address), + "deadline": self._deadline(), + } + ], + ) + print(position) + + multicall = await positionManager.functions.multicall([position]).transact( + {"from": _addr_to_str(self.address), "gas": Wei(417918)} + ) + + print(multicall) + # mint_position = positionManager.functions.mint({'token0':token0,'token1':token1,'fee':fee,'tickLower':MIN_TICK,'tickUpper':MAX_TICK, + # 'amount0Desired':amount0,'amount1Desired':amount1,'amount0Min':0,'amount1Min':0,'recipient':_addr_to_str(self.address),'deadline':self._deadline() + # }) + + # mint_tx = self._build_and_send_tx(mint_position) + # self.w3.eth.wait_for_transaction_receipt(mint_tx, timeout=6000) + # + # tx2 = self._build_and_send_tx(multicall,) + # self.w3.eth.wait_for_transaction_receipt(tx2, timeout=6000) + + # position = positionManager.functions.mint().buildTransaction() + # print(position['data']) + + return multicall + + def encode_v3_path( + self, route: list[AddressLike], fees: list[int], is_exact_out: bool = False + ) -> bytes: + """ + Encodes a Uniswap V3 path for use in swap and quote functions. + + :param route : A list of token addresses representing the swap route. The length of this list must be one greater than the length of the `fees` list. + :param fees : A list of fee tiers corresponding to each hop in the route. Each fee tier should be an integer representing the fee in hundredths of a bip (e.g., 3000 for 0.3%). + :param is_exact_out : A boolean indicating whether the swap is an exact output swap. If True, the path will be encoded in reverse order. + + :return : A bytes object representing the encoded path. + """ + if len(route) < 2: + raise ValueError( + "Route must contain at least two token addresses. " + f"Got route length {len(route)}." + ) + if len(route) != len(fees) + 1: + raise ValueError( + "Route length must be one greater than fees length. " + f"Got route length {len(route)} and fees length {len(fees)}." + ) + + path = b"" + _route = route.copy() + _fees = fees.copy() + if is_exact_out: + # For exact output swaps, the path is encoded in reverse order + _route.reverse() + _fees.reverse() + + for i in range(len(_fees)): + path += self.w3.to_bytes(hexstr=HexStr(_addr_to_str(_route[i]))) + _fees[ + i + ].to_bytes(3, "big") + path += self.w3.to_bytes(hexstr=HexStr(_addr_to_str(_route[-1]))) + + return path + + async def get_raw_price( + self, token_in: AddressLike, token_out: AddressLike, fee: int | None = None + ) -> float: + """ + Returns current price for pair of tokens [token_in, token_out] regrading liquidity that is being locked in the pool + Parameter `fee` is required for V3 only, can be omitted for V2 + Requires pair [token_in, token_out] having direct pool + """ + fee = validate_fee_tier(fee=fee, version=self.version) + + if token_in == ETH_ADDRESS: + token_in = await self.get_weth_address() + if token_out == ETH_ADDRESS: + token_out = await self.get_weth_address() + + if self.version == 2: + params: Iterable[ChecksumAddress | int | None] = [ + self.w3.to_checksum_address(token_in), + self.w3.to_checksum_address(token_out), + ] + pair_token = await self.factory_contract.functions.getPair(*params).call() + token_in_erc20 = _load_contract_erc20( + self.w3, self.w3.to_checksum_address(token_in) + ) + token_in_balance = int( + await token_in_erc20.functions.balanceOf( + self.w3.to_checksum_address(pair_token) + ).call() + ) + token_in_decimals = (await self.get_token(token_in)).decimals + token_in_balance = token_in_balance / (10**token_in_decimals) + + token_out_erc20 = _load_contract_erc20( + self.w3, self.w3.to_checksum_address(token_out) + ) + token_out_balance = int( + await token_out_erc20.functions.balanceOf( + self.w3.to_checksum_address(pair_token) + ).call() + ) + token_out_decimals = (await self.get_token(token_out)).decimals + token_out_balance = token_out_balance / (10**token_out_decimals) + + raw_price = token_out_balance / token_in_balance + else: + params = [ + self.w3.to_checksum_address(token_in), + self.w3.to_checksum_address(token_out), + fee, + ] + pool_address = await self.factory_contract.functions.getPool(*params).call() + pool_contract = _load_contract( + self.w3, abi_name="uniswap-v3/pool", address=pool_address + ) + # t0 = pool_contract.functions.token0().call() + t1 = await pool_contract.functions.token1().call() + if t1.lower() == token_in.lower(): + den0 = (await self.get_token(token_in)).decimals + den1 = (await self.get_token(token_out)).decimals + else: + den0 = (await self.get_token(token_out)).decimals + den1 = (await self.get_token(token_in)).decimals + sqrtPriceX96 = (await pool_contract.functions.slot0().call())[0] + raw_price = (sqrtPriceX96 * sqrtPriceX96 * 10**den1 >> (96 * 2)) / ( + 10**den0 + ) + if t1.lower() == token_in.lower(): + raw_price = 1 / raw_price + return raw_price + + async def estimate_price_impact( + self, + token_in: AddressLike, + token_out: AddressLike, + amount_in: int, + fee: int, + route: list[AddressLike] | None = None, + ) -> float: + """ + Returns the estimated price impact as a positive float (0.01 = 1%). + + NOTE: Work-in-progress. + + See ``examples/price_impact.py`` for an example which uses this. + """ + try: + price_small = await self.get_raw_price( + token_in, + token_out, + fee=fee, + ) + except (ArithmeticError, BadFunctionCallOutput): + # ArithmeticError is raised when `token_in` amount in the pool equals 0. + # BadFunctionCallOutput is raised when the pool's contract for given `(token_in, token_out, fee)` hasn't been deployed + return 1 + + if price_small == 0: + # Occurs when `token_out` amount in the pool equals 0 + return 1 + try: + cost_amount = await self.get_price_input( + token_in, token_out, amount_in, fee=fee, route=route + ) + except ContractLogicError: + # ContractLogicError is raised when the pool's contract for given `(token_in, token_out, fee)` hasn't been deployed. + # As `get_price_input()` uses UniswapV3Quoter for getting prices, that contract raises such exception in this situation. + return 1 + price_amount = ( + cost_amount + / (amount_in / (10 ** (await self.get_token(token_in)).decimals)) + ) / 10 ** (await self.get_token(token_out)).decimals + + # calculate and subtract the realised fees from the price impact. See: + # https://github.com/uniswap-python/uniswap-python/issues/310 + # The fee calculation will need to be updated when adding support for the AutoRouter. + price_impact_with_fees = float((price_small - price_amount) / price_small) + fee_realised_percentage = realised_fee_percentage(fee, amount_in) + price_impact_real = price_impact_with_fees - fee_realised_percentage + + return price_impact_real + + # ------ Exchange ------------------------------------------------------------------ + def get_fee_maker(self) -> float: + """Get the maker fee.""" + return 0 + + def get_fee_taker(self) -> float: + """Get the taker fee.""" + return 0.003 + + # ---- Old v1 utils ---- + + async def _exchange_address_from_token( + self, token_addr: AddressLike + ) -> AddressLike: + ex_addr: AddressLike = await self.factory_contract.functions.getExchange( + token_addr + ).call() + # TODO: What happens if the token doesn't have an exchange/doesn't exist? + # Should probably raise an Exception (and test it) + return ex_addr + + async def _token_address_from_exchange(self, exchange_addr: AddressLike) -> Address: + token_addr: Address = await ( + (await self._exchange_contract(ex_addr=exchange_addr)) + .functions.tokenAddress(exchange_addr) + .call() + ) + return token_addr + + async def _exchange_contract( + self, + token_addr: AddressLike | None = None, + ex_addr: AddressLike | None = None, + ) -> AsyncContract: + if not ex_addr and token_addr: + ex_addr = await self._exchange_address_from_token(token_addr) + if ex_addr is None: + raise InvalidToken(token_addr) + abi_name = "uniswap-v1/exchange" + contract = _load_contract(self.w3, abi_name=abi_name, address=ex_addr) + logger.info(f"Loaded exchange contract {contract} at {contract.address}") + return contract + + async def _get_all_tokens(self) -> list[ERC20Token]: + """ + Retrieves all token pairs. + + Note: This is a *very* expensive operation and might therefore not work properly. + """ + # FIXME: This is a very expensive operation, would benefit greatly from caching. + tokenCount = await self.factory_contract.functions.tokenCount().call() + tokens = [] + for i in range(tokenCount): + address = await self.factory_contract.functions.getTokenWithId(i).call() + if address == "0x0000000000000000000000000000000000000000": + # Token is ETH + continue + token = await self.get_token(address) + tokens.append(token) + return tokens + + +def _load_contract(w3: AsyncWeb3, abi_name: str, address: AddressLike) -> AsyncContract: + address = Web3.to_checksum_address(address) + return w3.eth.contract(address=address, abi=_load_abi(abi_name)) + + +def _load_contract_erc20(w3: AsyncWeb3, address: AddressLike) -> AsyncContract: + return _load_contract(w3, "erc20", address) diff --git a/uniswap/asyncuniswap4.py b/uniswap/asyncuniswap4.py new file mode 100644 index 00000000..1ede66e8 --- /dev/null +++ b/uniswap/asyncuniswap4.py @@ -0,0 +1,3501 @@ +import asyncio +import logging +import os +import time +from dataclasses import astuple +from decimal import Decimal + +import eth_abi.abi +from eth_abi import encode +from eth_abi.packed import encode_packed +from web3 import AsyncWeb3, Web3 +from web3.contract import AsyncContract +from web3.contract.async_contract import AsyncContractFunction +from web3.exceptions import BadFunctionCallOutput, ContractLogicError, NameNotFound +from web3.types import ( + HexBytes, + Nonce, + TxParams, + Wei, +) + +from .constants import ( + ETH_ADDRESS, + Q96, + ZERO_HOOK, + _netid_to_name, + _permit2_contract_addresses_v4, + _poolmanager_contract_addresses_v4, + _position_descriptor_contract_addresses_v4, + _position_manager_contract_addresses_v4, + _quoter_contract_addresses_v4, + _reserves_lens_contract_addresses_v4, + _router_contract_addresses_v4, + _stateview_contract_addresses_v4, + universal_router_commands, + universal_router_commands_abis, + v4_actions, + v4_actions_abis, +) +from .exceptions import InvalidToken +from .token import ERC20Token +from .types import ( + AddressLike, + ModifyLiquidityParams, + PathKey, + PermitBatch, + PermitSingle, + PoolKey, + SwapParams, +) +from .util import ( + _addr_to_str, + _load_abi, + _str_to_addr, + get_sqrt_ratio_at_tick, + realised_fee_percentage, +) + +logger = logging.getLogger(__name__) + + +class AsyncUniswap4: + """ + Wrapper around Uniswap v4 contracts. + """ + + w3: AsyncWeb3 + address: AddressLike + last_nonce: Nonce + + def __init__( + self, + address: str | AddressLike, + private_key: str | None = None, + provider: str | None = None, + web3: AsyncWeb3 | None = None, + max_slippage: float = 0.01, + gas_limit: float = 250000.0, + gas_price: float = 1.80, + priority_fee: float = 1.0, + post_merge: bool = True, + ) -> None: + """ + :param address: The public address of the ETH wallet to use. + :param private_key: The private key of the ETH wallet to use. + :param provider: Can be optionally set to a Web3 provider URI. If none set, will fall back to the PROVIDER environment variable, or web3 if set. + :param web3: Can be optionally set to a custom Web3 instance. + :param max_slippage: Maximum slippage for a trade, as a float (0.01 is 1%). + :param gas_limit: Maximum gas amount allocated for transactions. + :param gas_price: Cost per unit of gas, in GWei. + :param priority_fee: Amount of ETH to pay to the block producers, in GWei. Affects tx position in the block, the bigger value, the higher position is. + :param post_merge: True is for post-Merge transations, False for legacy ones. + """ + + self.address: AddressLike = ( + _str_to_addr(address) if isinstance(address, str) else address + ) + self.private_key = private_key + + self.max_slippage = max_slippage + + self.provider = provider or os.environ["PROVIDER"] + if web3: + self.w3 = web3 + else: + self.w3 = AsyncWeb3( + AsyncWeb3.AsyncHTTPProvider( + self.provider, request_kwargs={"timeout": 60} + ) + ) + temp_w3 = Web3(Web3.HTTPProvider(self.provider, request_kwargs={"timeout": 60})) + self.last_nonce = temp_w3.eth.get_transaction_count(self.address) + + # This code automatically approves you for trading on the exchange. + # max_approval is to allow the contract to exchange on your behalf. + # max_approval_check checks that current approval is above a reasonable + # number + # The program cannot check for max_approval each time because it + # decreases with each trade. + self.max_approval_hex = f"0x{64 * 'f'}" + self.max_approval_int = int(self.max_approval_hex, 16) + self.max_approval_check_hex = f"0x{15 * '0'}{49 * 'f'}" + self.max_approval_check_int = int(self.max_approval_check_hex, 16) + self.gas_limit = gas_limit + self.gas_price = gas_price + self.post_merge = post_merge + self.priority_fee = priority_fee + + chain_id = int(temp_w3.net.version) + self.net_id = chain_id + if self.net_id in _netid_to_name: + self.net_name = _netid_to_name[self.net_id] + if self.net_name not in _quoter_contract_addresses_v4: + raise ValueError(f"Netid {self.net_id} is not supported yet.") + else: + raise ValueError(f"Unknown netid: {self.net_id}") + + logger.info(f"Using {self.w3} ('{self.net_name}', netid: {self.net_id})") + quoter_address = _quoter_contract_addresses_v4[self.net_name] + router_address = _router_contract_addresses_v4[self.net_name] + stateview_address = _stateview_contract_addresses_v4[self.net_name] + permit2_address = _permit2_contract_addresses_v4[self.net_name] + position_descriptor_address = _position_descriptor_contract_addresses_v4[ + self.net_name + ] + pool_manager_address = _poolmanager_contract_addresses_v4[self.net_name] + position_manager_address = _position_manager_contract_addresses_v4[ + self.net_name + ] + reserves_lens_address = _reserves_lens_contract_addresses_v4[self.net_name] + + """ + NOTE: Following exception handling relates to the testnets only, as production contracts are always deployed. + Specifically, PositionDescriptor is not deployed on Sepolia and Unichain Sepolia yet, so loading it will raise an error. + However, this is a periphery helper which is not critical for core functionality. + """ + try: + self.quoter_address = _str_to_addr(quoter_address) + self.quoter = _load_contract( + self.w3, abi_name="uniswap-v4/quoter", address=self.quoter_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading quoter contract: {e}") + + try: + self.router_address = _str_to_addr(router_address) + self.router = _load_contract( + self.w3, abi_name="uniswap-v4/router", address=self.router_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading router contract: {e}") + + try: + self.stateview_address = _str_to_addr(stateview_address) + self.stateview = _load_contract( + self.w3, abi_name="uniswap-v4/stateview", address=self.stateview_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading stateview contract: {e}") + + try: + self.permit2_address = _str_to_addr(permit2_address) + self.permit2 = _load_contract( + self.w3, abi_name="uniswap-v4/permit2", address=self.permit2_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading permit2 contract: {e}") + + try: + self.position_descriptor_address = _str_to_addr(position_descriptor_address) + self.position_descriptor = _load_contract( + self.w3, + abi_name="uniswap-v4/pos_descriptor", + address=self.position_descriptor_address, + ) + except NameNotFound as e: + logger.error( + f"Error occurred while loading position descriptor contract: {e}" + ) + + try: + self.pool_manager_address = _str_to_addr(pool_manager_address) + self.pool_manager = _load_contract( + self.w3, + abi_name="uniswap-v4/poolmanager", + address=self.pool_manager_address, + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading pool manager contract: {e}") + + try: + self.position_manager_address = _str_to_addr(position_manager_address) + self.position_manager = _load_contract( + self.w3, + abi_name="uniswap-v4/pos_manager", + address=self.position_manager_address, + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading position manager contract: {e}") + + try: + self.reserves_lens_address = _str_to_addr(reserves_lens_address) + self.reserves_lens = _load_contract( + self.w3, + abi_name="uniswap-v4/reserves_lens", + address=self.reserves_lens_address, + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading reserves lens contract: {e}") + + # Approvals + async def approve( + self, + token: AddressLike, + max_approval: int | None = None, + delay_interval: int | None = 7, + approve_position_manager: bool = False, + ) -> HexBytes: + """Approve the router to spend a token on the user's behalf, or set up a permit for the position manager to pull the token from the user's wallet. For ETH, the router can pull from the user's wallet directly, so no approval is necessary. + + :param token: The address of the token to approve. + :param max_approval: Optional. The maximum amount to approve. If not set, will approve a maximum possible amount. + :param delay_interval: Optional. Seconds to wait between two approval transactions. Defaults to 7. Values less than 1 are treated as default. + :param approve_position_manager: Optional. Whether to approve the position manager to spend the token. Defaults to False. + """ + + # If the token is not ETH, approve the router to spend it. For ETH, the router can pull from the user's wallet directly, so no approval is necessary. + if _addr_to_str(token) != ETH_ADDRESS: + max_approval = self.max_approval_int if not max_approval else max_approval + function = self.erc20_contract(token).functions.approve( + _addr_to_str(self.permit2_address), max_approval + ) + logger.info(f"Approving {_addr_to_str(token)} for PERMIT2...") + tx = await self._build_and_send_tx(function) + if delay_interval is None or delay_interval < 1: + delay_interval = 7 + await asyncio.sleep(delay_interval) + else: + raise ValueError("ETH needs no approval.") + # Give an exchange/router max approval for a token. + max_approval = 2**100 - 1 + expiration: int = 10**12 + logger.info(f"Setting permit for {_addr_to_str(token)} at router contract...") + function = self.permit2.functions.approve( + _str_to_addr(token), self.router_address, max_approval, expiration + ) + tx = await self._build_and_send_tx(function) + + if approve_position_manager: + await asyncio.sleep(delay_interval) + max_approval = 2**100 - 1 + expiration = 10**12 + logger.info( + f"Setting permit for {_addr_to_str(token)} at position manager contract..." + ) + function = self.permit2.functions.approve( + _str_to_addr(token), + self.position_manager_address, + max_approval, + expiration, + ) + tx = await self._build_and_send_tx(function) + return tx + + async def approval(self, token: AddressLike) -> int: + """Returns the current allowance for the router to spend a token on the user's behalf. Note that this is not the allowance of the token itself, but the allowance set in the Permit2 contract for the router to spend the token.""" + # [0]=current allowance, [1]=allowance expiration [2]=current nonce + result = int( + ( + await self.permit2.functions.allowance( + self.address, token, self.router.address + ).call() + )[0] + ) + return result + + # Transaction parameters customization + # Gas limit + def get_gas_limit(self) -> float: + """Returns the current gas limit for transactions.""" + return self.gas_limit + + def set_gas_limit(self, gas_limit: float) -> None: + """Sets the gas limit for transactions.""" + self.gas_limit = gas_limit + + # Gas price in GWei + def get_gas_price(self) -> float: + """Returns the current gas price in GWei.""" + return self.gas_price + + def set_gas_price(self, gas_price: float) -> None: + """Sets the gas price in GWei.""" + self.gas_price = gas_price + + # Priority fee in GWei + def get_gas_priorityfee(self) -> float: + """Returns the current priority fee in GWei.""" + return self.priority_fee + + def set_gas_priorityfee(self, priority_fee: float) -> None: + """Sets the priority fee in GWei.""" + self.priority_fee = priority_fee + + # Slippage + def get_max_slippage(self) -> float: + """Returns the current maximum slippage as a float (0.01 is 1%).""" + return self.max_slippage + + def set_max_slippage(self, max_slippage: float) -> None: + """Sets the maximum slippage as a float (0.01 is 1%).""" + self.max_slippage = max_slippage + + # Nonce management + async def update_last_nonce(self) -> None: + """Updates the last nonce to the current nonce of the wallet. This can be used to resync the nonce if transactions have been sent outside of this class or custom nonce is used.""" + self.last_nonce = await self.w3.eth.get_transaction_count(self.address) + + # StateView methods + async def stateview_get_fee_growth_globals( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ) -> dict: + """ + Retrieves the global fee growth of a pool. + """ + if token0.lower() > token1.lower(): + token0, token1 = token1, token0 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + fee_growth_globals: dict = await self.stateview.functions.getFeeGrowthGlobals( + pool_id + ).call() + return_value = { + "feeGrowthGlobal0": fee_growth_globals[0], + "feeGrowthGlobal1": fee_growth_globals[1], + } + return return_value + + async def stateview_get_fee_growth_inside( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick_lower: int, + tick_upper: int, + ) -> dict: + """ + Calculates the fee growth inside a tick range of a pool + """ + if token0.lower() > token1.lower(): + token0, token1 = token1, token0 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + fee_growth_inside: dict = await self.stateview.functions.getFeeGrowthInside( + pool_id, tick_lower, tick_upper + ).call() + return_value = { + "feeGrowthInside0X128": fee_growth_inside[0], + "feeGrowthInside1X128": fee_growth_inside[1], + } + return return_value + + async def stateview_get_liquidity( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ) -> int: + """Retrieves the total liquidity of a pool.""" + if token0.lower() > token1.lower(): + token0, token1 = token1, token0 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + liquidity: int = await self.stateview.functions.getLiquidity(pool_id).call() + return liquidity + + async def stateview_get_position_info( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + owner: str, + tick_lower: int, + tick_upper: int, + token_id: int, + ) -> dict: + """ + Retrieves position info in a pool. + + :param token_id: TokenID of the correspoding NFT + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + salt = HexBytes(token_id.to_bytes(32, byteorder="big")) + position_info: dict = await self.stateview.functions.getPositionInfo( + pool_id, owner, tick_lower, tick_upper, salt + ).call() + return_value = { + "liquidity": position_info[0], + "feeGrowthInside0LastX128": position_info[1], + "feeGrowthInside1LastX128": position_info[2], + } + return return_value + + async def stateview_get_slot0( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ) -> dict: + """ + Returns current state of the pool. + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + slot: dict = await self.stateview.functions.getSlot0(pool_id).call() + return_value = { + "sqrtPriceX96": slot[0], + "tick": slot[1], + "protocolFee": slot[2], + "lpFee": slot[3], + } + return return_value + + async def stateview_get_tick_bitmap( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick: int, + ) -> int: + """ + Retrieves the tick bitmap of a pool at a specific tick. + + :param tick: MUST be int16 + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + tick_bitmap: int = await self.stateview.functions.getTickBitmap( + pool_id, tick + ).call() + return tick_bitmap + + async def stateview_get_tick_fee_growth_outside( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick: int, + ) -> dict: + """ + Retrieves the fee growth outside a tick range of a pool + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + fee_growth_outside: dict = ( + await self.stateview.functions.getTickFeeGrowthOutside(pool_id, tick).call() + ) + return_value = { + "feeGrowthOutside0X128": fee_growth_outside[0], + "feeGrowthOutside1X128": fee_growth_outside[1], + } + return return_value + + async def stateview_get_tick_pool_info( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick: int, + ) -> dict: + """ + Retrieves the tick information of a pool at a specific tick. + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + tick_info: dict = await self.stateview.functions.getTickInfo( + pool_id, tick + ).call() + return_value = { + "liquidityGross": tick_info[0], + "liquidityNet": tick_info[1], + "feeGrowthOutside0X128": tick_info[2], + "feeGrowthOutside1X128": tick_info[3], + } + return return_value + + # ReservesLens methods + async def reserves_lens_get_pool_tvl( + self, pool_key: PoolKey, custom_provider: str = "" + ) -> dict: + """ + Retrieves the total value locked (TVL) of a pool. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_key: The PoolKey object representing the pool. + :param custom_provider: The custom provider address, empty string for default. + :returns: A dictionary containing the reserves of the pool. + """ + if custom_provider == "": + reserves: dict = await self.reserves_lens.functions.getPoolTVL( + _addr_to_str(self.pool_manager_address), astuple(pool_key) + ).call() + else: + reserves = await self.reserves_lens.functions.getPoolTVL( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + custom_provider, + ).call() + return_value = { + "coreAmount0": reserves[0], + "coreAmount1": reserves[1], + "hookReserves0": reserves[2], + "hookReserves1": reserves[3], + "hookEffective0": reserves[4], + "hookEffective1": reserves[5], + "sqrtPriceX96": reserves[6], + "tick": reserves[7], + "activeLiquidity": reserves[8], + "blockNumber": reserves[9], + "statsProvider": reserves[10], + "hookPermissions": reserves[11], + "hasCustomAccounting": reserves[12], + "statsStatus": reserves[13], + } + return return_value + + async def reserves_lens_get_pool_tvl_batch( + self, pool_keys: list[PoolKey], custom_provider: list[str] | None = None + ) -> list[dict]: + """ + Retrieves the total value locked (TVL) of multiple pools in a batch. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_keys: A list of PoolKey objects representing the pools. + :param custom_provider: The custom provider addresses for each pool. If None, will use default for all pools. + :returns: A list of dictionaries containing the reserves of each pool. + """ + if custom_provider is None: + custom_provider = [""] * len(pool_keys) + + reserves_list: list = await self.reserves_lens.functions.getPoolTVLBatch( + _addr_to_str(self.pool_manager_address), + [astuple(pool_key) for pool_key in pool_keys], + ).call() + else: + reserves_list = await self.reserves_lens.functions.getPoolTVLBatch( + _addr_to_str(self.pool_manager_address), + [astuple(pool_key) for pool_key in pool_keys], + custom_provider, + ).call() + + return_value = [] + for reserves in reserves_list: + return_value.append( + { + "coreAmount0": reserves[0], + "coreAmount1": reserves[1], + "hookReserves0": reserves[2], + "hookReserves1": reserves[3], + "hookEffective0": reserves[4], + "hookEffective1": reserves[5], + "sqrtPriceX96": reserves[6], + "tick": reserves[7], + "activeLiquidity": reserves[8], + "blockNumber": reserves[9], + "statsProvider": reserves[10], + "hookPermissions": reserves[11], + "hasCustomAccounting": reserves[12], + "statsStatus": reserves[13], + } + ) + + return return_value + + async def reserves_lens_get_tvl_paged( + self, + pool_key: PoolKey, + cursor: bytes, + custom_provider: str = "", + max_reads: int = 0, + ) -> tuple[dict, bytes, bool]: + """ + Retrieves the total value locked (TVL) of multiple pools in a paged manner. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_key: A list of PoolKey objects representing the pools. + :param cursor: The cursor for paged retrieval. + :param custom_provider: The custom provider address, empty string for default. + :param max_reads: The maximum number of reads to perform using custom provider. + :returns: A tuple containing the reserves dictionary, the next cursor, and a boolean indicating if the paged retrieval is done. + """ + if custom_provider == "": + ( + reserves_tuple, + next_cursor, + done, + ) = await self.reserves_lens.functions.getPoolTVLPaged( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + cursor, + ).call() + else: + ( + reserves_tuple, + next_cursor, + done, + ) = await self.reserves_lens.functions.getPoolTVLPaged( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + custom_provider, + cursor, + max_reads, + ).call() + + reserves: dict = { + "coreAmount0": reserves_tuple[0], + "coreAmount1": reserves_tuple[1], + "hookReserves0": reserves_tuple[2], + "hookReserves1": reserves_tuple[3], + "hookEffective0": reserves_tuple[4], + "hookEffective1": reserves_tuple[5], + "sqrtPriceX96": reserves_tuple[6], + "tick": reserves_tuple[7], + "activeLiquidity": reserves_tuple[8], + "blockNumber": reserves_tuple[9], + "statsProvider": reserves_tuple[10], + "hookPermissions": reserves_tuple[11], + "hasCustomAccounting": reserves_tuple[12], + "statsStatus": reserves_tuple[13], + } + + return_value = (reserves, next_cursor, done) + return return_value + + async def reserves_lens_get_populated_ticks_in_word( + self, pool_key: PoolKey, word_position: int + ) -> list[dict]: + """ + Retrieves the populated ticks in a specific word of a pool. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_key: The PoolKey object representing the pool. + :param word_position: The position of the word to retrieve. + :returns: A list of dictionaries containing the populated ticks. + """ + results: list = await self.reserves_lens.functions.getPopulatedTicksInWord( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + word_position, + ).call() + return_value: list[dict] = [] + for result in results: + return_value.append( + { + "tick": result[0], + "liquidityNet": result[1], + "liquidityGross": result[2], + } + ) + return return_value + + # PositionDescriptor methods + async def position_descriptor_get_currency_ratio_priority( + self, currency: str + ) -> int: + """ + For certain currencies on mainnet, the smaller the currency, the higher the priority. + And those with the higher priority values (more positive values) will be in the numerator of the price ratio + + :returns: The priority of a currency. + """ + ratio_priority: int = int( + await self.position_descriptor.functions.currencyRatioPriority( + currency + ).call() + ) + return_value = ratio_priority + return return_value + + async def position_descriptor_get_flip_ratio( + self, currency0: str, currency1: str + ) -> bool: + """ + :returns: True if currency0 has higher priority than currency1 + """ + flip_ratio: bool = bool( + await self.position_descriptor.functions.flipRatio( + currency0, currency1 + ).call() + ) + return_value = flip_ratio + return return_value + + async def position_descriptor_get_native_currency_label(self) -> str: + """ + :returns: The label for the native currency as a string + """ + native_currency_label: str = str( + await self.position_descriptor.functions.nativeCurrencyLabel().call() + ) + return_value = native_currency_label + return return_value + + async def position_descriptor_get_pool_manager(self) -> str: + """ + :returns: PoolManager address as a string + """ + pool_manager: str = str( + await self.position_descriptor.functions.poolManager().call() + ) + return_value = pool_manager + return return_value + + async def position_descriptor_get_token_uri( + self, pos_manager: str, token_id: int + ) -> str: + """ + Produces the URI describing a particular token ID + Note this URI may be a data: URI with the JSON contents directly inlined + + :returns: The URI of the ERC721-compliant metadata + """ + token_uri: str = str( + await self.position_descriptor.functions.tokenURI( + pos_manager, token_id + ).call() + ) + return_value = token_uri + return return_value + + async def position_descriptor_get_wrapped_native_address(self) -> str: + """ + :returns: The wrapped native currency address as a string + """ + wrapped_native_address: str = str( + await self.position_descriptor.functions.wrappedNative().call() + ) + return_value = wrapped_native_address + return return_value + + # PositionManager methods + # Read methods + async def position_manager_get_domain_separator( + self, + ) -> bytes: + """ + :returns: The domain separator for the current chain; bytes32 + """ + domain_separator: bytes = bytes( + await self.position_manager.functions.DOMAIN_SEPARATOR().call() + ) + return_value = domain_separator + return return_value + + async def position_manager_get_weth9( + self, + ) -> str: + """ + :returns: The wrapped native token address + """ + weth9: str = str(await self.position_manager.functions.WETH9().call()) + return_value = weth9 + return return_value + + async def position_manager_get_balance_of(self, address: str) -> int: + """ + :returns: The number of tokens in owner's address. + """ + balance: int = int( + await self.position_manager.functions.balanceOf(address).call() + ) + return_value = balance + return return_value + + async def position_manager_get_approved(self, token_id: int) -> str: + """ + :returns: The account approved for a token. + """ + operator: str = str( + await self.position_manager.functions.getApproved(token_id).call() + ) + return_value = operator + return return_value + + async def position_manager_get_pool_and_position_info(self, token_id: int) -> dict: + """ + :returns: The PoolKey class object and position info of a position + """ + ( + pool_key_tuple, + info, + ) = await self.position_manager.functions.getPoolAndPositionInfo( + token_id + ).call() + pool_key: PoolKey = PoolKey(*pool_key_tuple) + return_value = { + "poolKey": pool_key, + "info": info, + } + return return_value + + async def position_manager_get_position_liquidity(self, token_id: int) -> int: + """ + :returns: The liquidity of a position + """ + position_liquidity: int = int( + await self.position_manager.functions.getPositionLiquidity(token_id).call() + ) + return_value = position_liquidity + return return_value + + async def position_manager_get_is_approved_for_all( + self, owner: str, operator: str + ) -> bool: + """ + :returns: True if the operator is allowed to manage all of the assets of owner + """ + is_approved_for_all: bool = bool( + await self.position_manager.functions.isApprovedForAll( + owner, operator + ).call() + ) + return_value = is_approved_for_all + return return_value + + async def position_manager_get_msg_sender( + self, + ) -> str: + """ + :returns: address considered executor of the actions + + The other context functions, _msgData and _msgValue, are not supported by this contract. + In many contracts this will be the address that calls the initial entry point + that calls `_executeActions` `msg.sender` shouldn't be used, as this will be + the v4 pool manager contract that calls `unlockCallback` + If using ReentrancyLock.sol, this function can return _getLocker() + """ + msg_sender: str = str(await self.position_manager.functions.msgSender().call()) + return_value = msg_sender + return return_value + + async def position_manager_get_name( + self, + ) -> str: + """ + :returns: The name of the PositionManager token + """ + name: str = str(await self.position_manager.functions.name().call()) + return_value = name + return return_value + + async def position_manager_get_next_token_id( + self, + ) -> int: + """ + :returns: The ID that will be used for the next minted liquidity position + """ + next_token_id: int = int( + await self.position_manager.functions.nextTokenId().call() + ) + return_value = next_token_id + return return_value + + async def position_manager_get_nonces(self, owner: str, word: int) -> int: + """ + :returns: Mapping of nonces consumed by each address, where a nonce is a single bit on the 256-bit bitmap + """ + bitmap: int = int( + await self.position_manager.functions.nonces(owner, word).call() + ) + return_value = bitmap + return return_value + + async def position_manager_get_owner_of(self, token_id: int) -> str: + """ + :returns: The owner of the position for a given token ID + """ + owner: str = str(await self.position_manager.functions.ownerOf(token_id).call()) + return_value = owner + return return_value + + async def position_manager_get_permit2( + self, + ) -> str: + """ + :returns: The Permit2 contract to forward approvals + """ + permit2: str = str(await self.position_manager.functions.permit2().call()) + return_value = permit2 + return return_value + + async def position_manager_get_pool_keys(self, pool_id_trunc: bytes) -> PoolKey: + """ + :param pool_id_trunc: The truncated ID of the pool, first 25 bytes of common pool_id + :returns: The PoolKey class object for a given token ID + """ + pool_keys_tuple = await self.position_manager.functions.poolKeys( + pool_id_trunc + ).call() + pool_keys: PoolKey = PoolKey(*pool_keys_tuple) + return_value = pool_keys + return return_value + + async def position_manager_get_position_info(self, token_id: int) -> int: + """ + :returns: The position info for a given token ID + """ + position_info: int = int( + await self.position_manager.functions.positionInfo(token_id).call() + ) + return_value = position_info + return return_value + + async def position_manager_get_subscriber(self, token_id: int) -> str: + """ + :returns: The subscriber of the position for a given token ID + """ + subscriber: str = str( + await self.position_manager.functions.subscriber(token_id).call() + ) + return_value = subscriber + return return_value + + async def position_manager_get_is_support_interface( + self, + interface_id: bytes, + ) -> bool: + """ + :param interface_id: The interface ID to check; should be 'bytes4' + :returns: True if specifeid interface is supported by the PositionManager contract + """ + if len(interface_id) != 4: + raise ValueError("interface_id should be 4 bytes long") + is_supported: bool = bool( + await self.position_manager.functions.supportsInterface(interface_id).call() + ) + return_value = is_supported + return return_value + + async def position_manager_get_symbol( + self, + ) -> str: + """ + :returns: The symbol of the PositionManager token + """ + symbol: str = str(await self.position_manager.functions.symbol().call()) + return_value = symbol + return return_value + + async def position_manager_get_token_descriptor( + self, + ) -> str: + """ + :returns: The address of the PositionDescriptor contract as a string + """ + token_descriptor: str = str( + await self.position_manager.functions.tokenDescriptor().call() + ) + return_value = token_descriptor + return return_value + + async def position_manager_get_position_uri(self, token_id: int) -> str: + """ + :returns: The URI of the position manager's ERC721-compliant metadata for a given token ID + """ + uri: str = str(await self.position_manager.functions.tokenURI(token_id).call()) + return_value = uri + return return_value + + async def position_manager_get_unsubscribe_gas_limit(self) -> int: + """ + :returns: The gas limit used when unsubscribing from a position. + """ + unsubscribe_gas_limit: int = ( + await self.position_manager.functions.unsubscribeGasLimit().call() + ) + return_value = unsubscribe_gas_limit + return return_value + + # Write methods + async def position_manager_approve(self, spender: str, token_id: int) -> HexBytes: + """ + Change or reaffirm the approved address for an NFT + Zero address removes existing approval. + """ + function = self.position_manager.functions.approve(spender, token_id) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def position_manager_initialize_pool( + self, pool_key: PoolKey, sqrt_price_x96: int, payable_amount: int + ) -> HexBytes: + """ + Initialize a Uniswap v4 Pool with the given parameters. + """ + function = self.position_manager.functions.initializePool( + astuple(pool_key), sqrt_price_x96 + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_modify_liquidities( + self, unlock_data: bytes, deadline: int, payable_amount: int + ) -> HexBytes: + """ + Unlocks Uniswap v4 PoolManager and batches actions for modifying liquidity + """ + function = self.position_manager.functions.modifyLiquidities( + unlock_data, deadline + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_modify_liquidities_without_unlock( + self, actions: bytes, params: list[bytes], payable_amount: int + ) -> HexBytes: + """ + Batches actions for modifying liquidity without unlocking v4 PoolManager + + This must be called by a contract that has already unlocked the v4 PoolManager + """ + function = self.position_manager.functions.modifyLiquiditiesWithoutUnlock( + actions, params + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_multicall( + self, data: list[bytes], payable_amount: int + ) -> HexBytes: + """ + Call multiple functions in the current contract in a single transaction, with the possibility of sending ETH along with the calls. + """ + function = self.position_manager.functions.multicall(data) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_permit( + self, + spender: str, + token_id: int, + deadline: int, + nonce: int, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Approve of a specific token ID for spending by spender via signature + """ + function = self.position_manager.functions.permit( + spender, token_id, deadline, nonce, signature + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_permit2_single( + self, + owner: str, + permit_single: PermitSingle, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Allows forwarding a single permit to permit2 + """ + function = self.position_manager.functions.permit( + owner, astuple(permit_single), signature + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_permit2_batch( + self, + owner: str, + permit_batch: PermitBatch, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Allows forwarding batch permits to permit2 + """ + function = self.position_manager.functions.permit( + owner, astuple(permit_batch), signature + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_permit_for_all( + self, + owner: str, + operator: str, + approved: bool, + deadline: int, + nonce: int, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Set an operator with full permission to an owner's tokens via signature + """ + function = self.position_manager.functions.permitForAll( + owner, operator, approved, deadline, nonce, signature + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_revoke_nonce( + self, nonce: int, payable_amount: int + ) -> HexBytes: + """ + Revoke a nonce by spending it, preventing it from being used again + """ + function = self.position_manager.functions.revokeNonce(nonce) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_safe_transfer_from( + self, from_addr: str, to_addr: str, token_id: int, payable_amount: int + ) -> HexBytes: + """ + Transfer a position from one address to another + """ + function = self.position_manager.functions.safeTransferFrom( + from_addr, to_addr, token_id + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_safe_transfer_from_with_data( + self, + from_addr: str, + to_addr: str, + token_id: int, + data: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Transfer a position from one address to another with additional data + """ + function = self.position_manager.functions.safeTransferFrom( + from_addr, to_addr, token_id, data + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_set_approval_for_all( + self, operator: str, approved: bool, payable_amount: int + ) -> HexBytes: + """ + Enable or disable approval for a third party ("operator") to manage all of `msg.sender`'s assets + """ + function = self.position_manager.functions.setApprovalForAll(operator, approved) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_subscribe( + self, token_id: int, new_subscriber: str, data: bytes, payable_amount: int + ) -> HexBytes: + """ + Enables the subscriber to receive notifications for a respective position + """ + function = self.position_manager.functions.subscribe( + token_id, new_subscriber, data + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_transfer_from( + self, from_addr: str, to_addr: str, token_id: int, payable_amount: int + ) -> HexBytes: + """ + Overrides solmate transferFrom in case a notification to subscribers is needed + """ + function = self.position_manager.functions.transferFrom( + from_addr, to_addr, token_id + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_unsubscribe( + self, token_id: int, payable_amount: int + ) -> HexBytes: + """ + Removes the subscriber from receiving notifications for a respective position + """ + function = self.position_manager.functions.unsubscribe(token_id) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + # PoolManager methods + # Read methods + async def pool_manager_get_allowance( + self, owner: str, spender: str, token_id: int + ) -> int: + """ + Spender allowance of an id. + """ + allowance: int = int( + await self.pool_manager.functions.allowance(owner, spender, token_id).call() + ) + return_value = allowance + return return_value + + async def pool_manager_get_balance_of(self, address: str, token_id: int) -> int: + """ + The number of tokens in owner's address. + """ + balance: int = int( + await self.pool_manager.functions.balanceOf(address, token_id).call() + ) + return_value = balance + return return_value + + async def pool_manager_get_extsload(self, slot: bytes) -> bytes: + """ + Called by external contracts to access granular pool state + """ + value: bytes = await self.pool_manager.functions.extsload(slot).call() + return_value = value + return return_value + + async def pool_manager_get_extsload_sequence( + self, start_slot: bytes, slots_count: int + ) -> list[bytes]: + """ + Called by external contracts to access a sequence of storage slots + """ + value: list[bytes] = await self.pool_manager.functions.extsload( + start_slot, slots_count + ).call() + return_value = value + return return_value + + async def pool_manager_get_extsload_sparse(self, slots: list[bytes]) -> list[bytes]: + """ + Called by external contracts to access a sparse set of storage slots + """ + value: list[bytes] = await self.pool_manager.functions.extsload(slots).call() + return_value = value + return return_value + + async def pool_manager_get_exttload_sparse(self, slots: list[bytes]) -> list[bytes]: + """ + Called by external contracts to access sparse transient pool state + """ + value: list[bytes] = await self.pool_manager.functions.exttload(slots).call() + return_value = value + return return_value + + async def pool_manager_get_exttload(self, slot: bytes) -> bytes: + """ + Called by external contracts to access transient storage of the contract + """ + value: bytes = await self.pool_manager.functions.exttload(slot).call() + return_value = value + return return_value + + async def pool_manager_get_is_operator(self, owner: str, operator: str) -> bool: + """ + Checks if a spender is approved by an owner as an operator + """ + is_operator: bool = await self.pool_manager.functions.isOperator( + owner, operator + ).call() + return_value = is_operator + return return_value + + async def pool_manager_get_owner(self) -> str: + """ + Retrieve the contract owner. + """ + owner: str = str(await self.pool_manager.functions.owner().call()) + return_value = owner + return return_value + + async def pool_manager_get_protocol_fee_controller(self) -> str: + """ + Returns the current protocol fee controller address + """ + protocol_fee_controller: str = str( + await self.pool_manager.functions.protocolFeeController().call() + ) + return_value = protocol_fee_controller + return return_value + + async def pool_manager_get_protocol_fees_accrued(self, address: str) -> int: + """ + Given a currency address, returns the protocol fees accrued in that currency. + """ + protocol_fees_accrued: int = int( + await self.pool_manager.functions.protocolFeesAccrued(address).call() + ) + return_value = protocol_fees_accrued + return return_value + + async def pool_manager_get_supports_interface(self, interface_id: bytes) -> bool: + """ + Checks if a given interface ID is supported by the contract + + :param interface_id: The interface ID to check; should be `bytes4` + :returns: True if specifeid interface is supported by the PoolManager contract + """ + if len(interface_id) != 4: + raise ValueError("interface_id should be 4 bytes long") + supports_interface: bool = bool( + await self.pool_manager.functions.supportsInterface(interface_id).call() + ) + return_value = supports_interface + return return_value + + # Write methods + async def pool_manager_approve( + self, spender: str, token_id: int, amount: int + ) -> HexBytes: + """ + Approves an amount of an id to a spender. + """ + function = self.pool_manager.functions.approve(spender, token_id, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_burn( + self, from_addr: str, token_id: int, amount: int + ) -> HexBytes: + """ + Called by the user to move value from ERC6909 balance. + """ + function = self.pool_manager.functions.burn(from_addr, token_id, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_clear(self, currency: str, amount: int) -> HexBytes: + """ + !!!WARNING!!! - Any currency that is cleared, will be non-retrievable, and locked in the contract permanently. + A call to clear will zero out a positive balance WITHOUT a corresponding transfer. + This could be used to clear a balance that is considered dust. + Additionally, the amount must be the exact positive balance. + This is to enforce that the caller is aware of the amount being cleared. + """ + function = self.pool_manager.functions.clear(currency, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_collect_protocol_fees( + self, recipient: str, currency: str, amount: int + ) -> HexBytes: + """ + Collects the protocol fees for a given recipient and currency, returning the amount collected + This will revert if the contract is unlocked + """ + function = self.pool_manager.functions.collectProtocolFees( + recipient, currency, amount + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_donate( + self, pool_key: PoolKey, amount0: int, amount1: int, hook_data: bytes + ) -> HexBytes: + """ + Donate the given currency amounts to the in-range liquidity providers of a pool + """ + function = self.pool_manager.functions.donate( + astuple(pool_key), amount0, amount1, hook_data + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_initialize( + self, pool_key: PoolKey, sqrt_price_x96: int + ) -> HexBytes: + """ + Initialize the state for a given pool ID. + """ + function = self.pool_manager.functions.initialize( + astuple(pool_key), sqrt_price_x96 + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_mint( + self, to_addr: str, token_id: int, amount: int + ) -> HexBytes: + """ + Called by the user to move value into ERC6909 balance. + """ + function = self.pool_manager.functions.mint(to_addr, token_id, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_modify_liquidity( + self, + pool_key: PoolKey, + liquidity_params: ModifyLiquidityParams, + hook_data: bytes, + ) -> HexBytes: + """ + Modify the liquidity for the given pool. + """ + function = self.pool_manager.functions.modifyLiquidity( + astuple(pool_key), astuple(liquidity_params), hook_data + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_set_operator( + self, operator: str, approved: bool + ) -> HexBytes: + """ + Sets or removes an operator for the caller. + """ + function = self.pool_manager.functions.setOperator(operator, approved) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_set_protocol_fee( + self, pool_key: PoolKey, new_protocol_fee: int + ) -> HexBytes: + """ + Sets the protocol fee for the given pool. + """ + function = self.pool_manager.functions.setProtocolFee( + astuple(pool_key), new_protocol_fee + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_set_protocol_fee_controller( + self, controller: str + ) -> HexBytes: + """ + Sets a new protocol fee controller. + """ + function = self.pool_manager.functions.setProtocolFeeController(controller) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_settle(self, payable_amount: int) -> HexBytes: + """ + Called by the user to pay what is owed. + """ + function = self.pool_manager.functions.settle() + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def pool_manager_settle_for( + self, recipient: str, payable_amount: int + ) -> HexBytes: + """ + Called by the user to pay on behalf of another address. + """ + function = self.pool_manager.functions.settleFor(recipient) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def pool_manager_swap( + self, pool_key: PoolKey, params: SwapParams, hook_data: bytes + ) -> HexBytes: + """ + Swap against the given pool. + """ + function = self.pool_manager.functions.swap( + astuple(pool_key), astuple(params), hook_data + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_sync(self, currency: str) -> HexBytes: + """ + Writes the current ERC20 balance of the specified currency to transient storage. + This is used to checkpoint balances for the manager and derive deltas for the caller. + This MUST be called before any ERC20 tokens are sent into the contract, see documentation for more details. + """ + function = self.pool_manager.functions.sync(currency) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_take( + self, currency: str, to_addr: str, amount: int + ) -> HexBytes: + """ + Called by the user to net out some value owed to the user. + """ + function = self.pool_manager.functions.take(currency, to_addr, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_transfer( + self, to_addr: str, token_id: int, amount: int + ) -> HexBytes: + """ + Transfers an amount of an id from the caller to a receiver. + """ + function = self.pool_manager.functions.transfer(to_addr, token_id, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_transfer_from( + self, sender: str, receiver: str, token_id: int, amount: int + ) -> HexBytes: + """ + Transfers an amount of an id from a sender to a receiver. + """ + function = self.pool_manager.functions.transferFrom( + sender, receiver, token_id, amount + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_transfer_ownership(self, new_owner: str) -> HexBytes: + """ + Transfers ownership of the contract to a new owner. + """ + function = self.pool_manager.functions.transferOwnership(new_owner) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_unlock(self, data: bytes) -> HexBytes: + """ + All interactions on the contract that account deltas require unlocking. + A caller that calls `unlock` must implement `IUnlockCallback(msg.sender).unlockCallback(data)`, + where they interact with the remaining functions on this contract. + """ + function = self.pool_manager.functions.unlock(data) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_update_dynamic_lp_fee( + self, pool_key: PoolKey, new_dynamic_lp_fee: int + ) -> HexBytes: + """ + Updates the pools lp fees for the a pool that has enabled dynamic lp fees. + """ + function = self.pool_manager.functions.updateDynamicLPFee( + astuple(pool_key), new_dynamic_lp_fee + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + # Tokens price functions + async def get_token_token_spot_price( + self, + token0: str, + token1: str, + fee: int = 500, + tick_spacing: int = 10, + hooks: str = ZERO_HOOK, + ) -> float: + """ + :param token0: The first token in the pair. + :param token1: The second token in the pair. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :return: The current spot price for token to token trades. + """ + + if token0.lower() < token1.lower(): + den0 = (await self.get_token(_str_to_addr(token0))).decimals + den1 = (await self.get_token(_str_to_addr(token1))).decimals + zero_for_one = True + else: + den0 = (await self.get_token(_str_to_addr(token1))).decimals + den1 = (await self.get_token(_str_to_addr(token0))).decimals + zero_for_one = False + + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + spot_price_x96: int = ( + await self.stateview_get_slot0(token0, token1, fee, tick_spacing, hooks) + )["sqrtPriceX96"] + + spot_price: float = (spot_price_x96 * spot_price_x96 * 10**den0 >> (96 * 2)) / ( + 10**den1 + ) + if not zero_for_one: + spot_price = 1 / spot_price + return spot_price + + # Estimates slippage for the given amount of token0 + async def estimate_price_impact( + self, + token0: str, + token1: str, + qty: int, + fee: int = 500, + tick_spacing: int = 10, + hooks: str = ZERO_HOOK, + hook_data: bytes = b"", + ) -> float: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token0` to be sold. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :return: the estimated price impact as a positive float (0.01 = 1%). + + See ``examples/v4_examples.py`` for an example which uses this. + + NOTE: Does not support multi-hop swaps, should be used for single-hop swaps only. + """ + + try: + spot_price = await self.get_token_token_spot_price( + token0, token1, fee, tick_spacing, hooks + ) + except (ArithmeticError, BadFunctionCallOutput): + # ArithmeticError is raised when `token0` amount in the pool + # equals 0. + # BadFunctionCallOutput is raised when the pool for + # given `(token0, token1, fee)` doesn't exist + return 1 + + if spot_price == 0: + # Occurs when `token1` amount in the pool equals 0 + return 1 + try: + quote_amount = await self.get_quote_exact_input_single( + token0, token1, qty, fee, tick_spacing, hooks, hook_data + ) + except ContractLogicError: + # ContractLogicError is raised when the pool's contract for given + # `(token0, token1, fee)` hasn't been deployed. + return 1 + price = ( + quote_amount + / (qty / (10 ** (await self.get_token(_str_to_addr(token0))).decimals)) + ) / 10 ** (await self.get_token(_str_to_addr(token1))).decimals + + # calculate and subtract the realised fees from the price impact. See: + # https://github.com/uniswap-python/uniswap-python/issues/310 + price_impact_with_fees: float = (spot_price - price) / spot_price + fee_realised_percentage: float = realised_fee_percentage(fee, qty) + price_impact_real: float = price_impact_with_fees - fee_realised_percentage + return price_impact_real + + # Quoter methods + # Read methods + async def get_quote_exact_input_single( + self, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str = ZERO_HOOK, + hook_data: bytes = b"", + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token0` to be sold. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :return: Quote for token to token single hop trades with an exact input. + """ + if token0.lower() < token1.lower(): + zero_for_one = True + else: + zero_for_one = False + token0, token1 = token1, token0 + pool_key = (token0, token1, fee, tick_spacing, hooks) + # [0]=The output quote [1]=estimated gas units used for the swap + quote_amount: int = ( + await self.quoter.functions.quoteExactInputSingle( + (pool_key, zero_for_one, qty, hook_data) + ).call() + )[0] + return quote_amount + + async def get_quote_exact_input( + self, + token_exact: str, + qty: int, + route: list[PoolKey], + ) -> int: + """ + :param token_exact: The token for which the qty parameter is specified. + :param qty: The amount of the token_exact to be swapped. + :param route: The list of PoolKeys representing the path of the swap, starting with the pool containing the input token and ending with the pool containing the output token. + :return: Quote for token to token multi-hop trades with an exact input. + """ + encoded_route = self.encode_path_keys_input(route, token_exact) + + # [0]=The output quote [1]=estimated gas units used for the swap + quote_amount: int = ( + await self.quoter.functions.quoteExactInput( + ( + token_exact, + [astuple(path_key) for path_key in encoded_route], + qty, + ) + ).call() + )[0] + return quote_amount + + async def get_quote_exact_output_single( + self, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str = ZERO_HOOK, + hook_data: bytes = b"", + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token1` to be bought. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :return: Quote for token to token single hop trades with an exact output. + """ + if token0.lower() < token1.lower(): + zero_for_one = True + else: + zero_for_one = False + token1, token0 = token0, token1 + + pool_key = ( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + # [0]=The input quote [1]=estimated gas units used for the swap + quote_amount: int = ( + await self.quoter.functions.quoteExactOutputSingle( + (pool_key, zero_for_one, qty, hook_data) + ).call() + )[0] + return quote_amount + + async def get_quote_exact_output( + self, + token_exact: str, + qty: int, + route: list[PoolKey], + ) -> int: + """ + :param token_exact: The token for which the qty parameter is specified, either the input or output token depending on the quote type. + :param qty: The amount of the token_exact to be swapped. For an exact output quote, this is the amount of output token desired. + :param route: The list of PoolKeys representing the path of the swap in reverse order, starting with the pool containing the output token and ending with the pool containing the input token. + :return: Quote for token to token multi-hop trades with an exact output. + """ + + encoded_route = self.encode_path_keys_output(route, token_exact) + quote_amount: int = ( + await self.quoter.functions.quoteExactOutput( + ( + token_exact, + [astuple(path_key) for path_key in encoded_route], + qty, + ) + ).call() + )[0] + return quote_amount + + # Market price functions for selling `qty` amount of `token0` to buy `token1` + async def get_price_input( + self, + token0: str, + token1: str, + qty: int, + fee: int | None = None, + tick_spacing: int | None = None, + hooks: str | None = ZERO_HOOK, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token0` to be sold. + :param fee: Optional. The fee tier for the swap. + + :param tick_spacing: Optional. The tick spacing for the swap. + :param hooks: Optional. The hooks for the swap. + :param hook_data: Optional. The hook data for the swap. + :param route: Optional. The route for the swap. The first element should be the pool containing `token0` and the last element should be the pool containing `token1`. + :return: Returns the exact amount of `token1` that can be received for `qty` amount of `token0` at the current price. + + If `route` is provided, it will be used for the quote. Otherwise, `fee` and `tick_spacing` must be provided for a single hop quote. + """ + result: int = 0 + if route is None: + if fee is None or tick_spacing is None: + raise ValueError( + "fee and tick_spacing parameters must be provided for single hop quotes" + ) + result = await self.get_quote_exact_input_single( + token0, + token1, + qty, + fee, + tick_spacing, + hooks, # type: ignore[arg-type] + hook_data, # type: ignore[arg-type] + ) + else: + result = await self.get_quote_exact_input(token0, qty, route) + return result + + async def get_price_output( + self, + token0: str, + token1: str, + qty: int, + fee: int | None = None, + tick_spacing: int | None = None, + hooks: str | None = ZERO_HOOK, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token1` to be bought. + :param fee: Optional. The fee tier for the swap. + :param tick_spacing: Optional. The tick spacing for the swap. + :param hooks: Optional. The hooks for the swap. + :param hook_data: Optional. The hook data for the swap. + :param route: Optional. The route for the swap in reverse order. The first element should be the pool containing `token1` and the last element should be the pool containing `token0`. + :return: Returns the exact amount of `token0` required to buy `qty` amount of `token1` at the current price. + + If `route` is provided, it will be used for the quote. Otherwise, `fee` and `tick_spacing` must be provided for a single hop quote. + """ + result: int = 0 + if route is None: + if fee is None or tick_spacing is None or hooks is None: + raise ValueError( + "fee, tick_spacing, and hooks parameters must be provided for single hop quotes" + ) + result = await self.get_quote_exact_output_single( + token0, + token1, + qty, + fee, + tick_spacing, + hooks, + hook_data, # type: ignore[arg-type] + ) + else: + result = await self.get_quote_exact_output(token1, qty, route) + return result + + # Swap functions + async def token_to_token_swap_exact_input( + self, + input_token: str, + qty: int, + qtycap: int, + output_token: str, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes = b"", + min_hop_price_x_36: int = 0, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param qty: The amount of `input_token` to be sold. + :param qtycap: The minimum amount of `output_token` that must be bought for the swap to succeed. + :param output_token: The token to be bought. + :param fee: The fee for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :param min_hop_price_x_36: Optional. The minimum price for each hop in the route, expressed as an integer. + :param custom_nonce: Optional. The nonce to use for the transaction. + + Swaps an exact amount of `input_token` for a minimum amount of `output_token`, + reverting if the amount of `output_token` received is less than `qtycap`. + """ + min_tokens_bought: int = int((1 - self.max_slippage) * qtycap) + + ether_amount: int = 0 + if input_token == ETH_ADDRESS: + ether_amount = qty + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8"], + [ + v4_actions["SWAP_EXACT_IN_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ) + + # SETTING PARAMS + if input_token.lower() < output_token.lower(): + zero_for_one = True + token0, token1 = input_token, output_token + else: + zero_for_one = False + token0, token1 = output_token, input_token + exact_input_single_params: bytes = encode( + v4_actions_abis["SWAP_EXACT_IN_SINGLE"], + [ + ( + (token0, token1, fee, tick_spacing, hooks), + zero_for_one, + qty, + min_tokens_bought, + min_hop_price_x_36, + hook_data, + ) + ], + ) + settle_all_params: bytes = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, qty], + ) + take_all_params: bytes = encode( + v4_actions_abis["TAKE_ALL"], + [output_token, min_tokens_bought], + ) + + # ENCODING DATA + params = [exact_input_single_params, settle_all_params, take_all_params] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return await self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + async def token_to_token_swap_input( + self, + input_token: str, + qty: int, + qtycap: int, + route: list[PoolKey], + min_hop_price_x_36: list[int] = [], + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param qty: The amount of `input_token` to be sold. + :param qtycap: The minimum amount of `output_token` that must be bought for the swap to succeed. + :param route: The list of PoolKeys representing the path of the swap. The first element should be the pool containing the `input_token` and the last element should be the pool containing the `output_token`. + :param min_hop_price_x_36: Optional. A list of minimum prices for each hop in the route, expressed as a list of integers. + :param custom_nonce: Optional. The nonce to use for the transaction. + + Swaps an exact amount of `input_token` for a minimum amount of `output_token` through a specified multi-hop route, + reverting if the amount of `output_token` received is less than `qtycap`. + """ + min_tokens_bought: int = int((1 - self.max_slippage) * qtycap) + + encoded_route = self.encode_path_keys_input(route, input_token) + + ether_amount: int = 0 + + if input_token == ETH_ADDRESS: + ether_amount = qty + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_IN, SETTLE_ALL, TAKE_ALL + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8"], + [ + v4_actions["SWAP_EXACT_IN"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ) + + # SETTING PARAMS + exact_input_params: bytes = encode( + v4_actions_abis["SWAP_EXACT_IN"], + [ + ( + input_token, + [astuple(path_key) for path_key in encoded_route], + min_hop_price_x_36, + qty, + min_tokens_bought, + ) + ], + ) + settle_all_params: bytes = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, qty], + ) + take_all_params: bytes = encode( + v4_actions_abis["TAKE_ALL"], + [ + _addr_to_str(encoded_route[-1].intermediate_currency), # type: ignore[arg-type] + min_tokens_bought, + ], + ) + + # ENCODING DATA + params = [exact_input_params, settle_all_params, take_all_params] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return await self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + async def token_to_token_swap_exact_output( + self, + input_token: str, + qty: int, + qtycap: int, + output_token: str, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes = b"", + min_hop_price_x_36: int = 0, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param qty: The amount of `output_token` to be bought. + :param qtycap: The maximum amount of `input_token` that can be sold for the trade to proceed. + :param output_token: The token to be bought. + :param fee: The fee for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :param min_hop_price_x_36: Optional. The minimum price for each hop in the route, expressed as an integer. + :param custom_nonce: Optional. The nonce for the transaction. + """ + + amount_in_max: int = int((1 + self.max_slippage) * qtycap) + + ether_amount: int = 0 + if input_token == ETH_ADDRESS: + ether_amount = amount_in_max + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_OUT_SINGLE, SETTLE_ALL, TAKE_ALL, TAKE + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + args=[ + v4_actions["SWAP_EXACT_OUT_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + v4_actions["TAKE"], + ], + ) + # SETTING PARAMS + if input_token.lower() < output_token.lower(): + zero_for_one = True + token0, token1 = input_token, output_token + else: + zero_for_one = False + token0, token1 = output_token, input_token + exact_output_single_params = encode( + v4_actions_abis["SWAP_EXACT_OUT_SINGLE"], + [ + ( + ( + token0, + token1, + fee, + tick_spacing, + hooks, + ), + zero_for_one, + qty, + amount_in_max, + min_hop_price_x_36, + hook_data, + ) + ], + ) + settle_all_params = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, amount_in_max], + ) + take_all_params = encode( + v4_actions_abis["TAKE_ALL"], + [output_token, qty], + ) + take_input_params = encode( + v4_actions_abis["TAKE"], [input_token, _addr_to_str(self.address), 0] + ) + + # ENCODING DATA + params = [ + exact_output_single_params, + settle_all_params, + take_all_params, + take_input_params, + ] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return await self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + async def token_to_token_swap_output( + self, + output_token: str, + qty: int, + qtycap: int, + route: list[PoolKey], + min_hop_price_x_36: list[int] = [], + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param output_token: The token to be bought. + :param qty: The amount of `output_token` to be bought. + :param qtycap: The maximum amount of `input_token` that can be sold for the trade to proceed. + :param route: The route for the swap in reverse order. The first element should be the pool containing `output_token` and the last element should be the pool containing `input_token`. + :param min_hop_price_x_36: Optional. The minimum price for each hop in the route, expressed as a list of integers. + :param custom_nonce: Optional. The nonce for the transaction. + + Swaps a maximum amount of `input_token` for an exact amount of `output_token` through a specified multi-hop route, + reverting if the amount of `input_token` required is more than `qtycap`. + """ + + amount_in_max: int = int((1 + self.max_slippage) * qtycap) + encoded_route = self.encode_path_keys_output(route, output_token) + + input_token: str = _addr_to_str(encoded_route[0].intermediate_currency) # type: ignore[arg-type] + + ether_amount: int = 0 + if input_token == ETH_ADDRESS: + ether_amount = amount_in_max + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_OUT, SETTLE_ALL, TAKE_ALL, TAKE + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + args=[ + v4_actions["SWAP_EXACT_OUT"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + v4_actions["TAKE"], + ], + ) + # SETTING PARAMS + exact_output_params: bytes = encode( + v4_actions_abis["SWAP_EXACT_OUT"], + [ + ( + output_token, + [astuple(path_key) for path_key in encoded_route], + min_hop_price_x_36, + qty, + amount_in_max, + ) + ], + ) + settle_all_params: bytes = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, amount_in_max], + ) + take_all_params: bytes = encode( + v4_actions_abis["TAKE_ALL"], + [output_token, qty], + ) + take_input_params = encode( + v4_actions_abis["TAKE"], [input_token, _addr_to_str(self.address), 0] + ) + + # ENCODING DATA + params = [ + exact_output_params, + settle_all_params, + take_all_params, + take_input_params, + ] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return await self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + async def drop_txn( + self, + address_to: AddressLike, + gas_price: float, + priority_fee: int = 10, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Replaces pending transaction with zero-value ETH transfer + + :param address_to: Zero address or any other valid address to which the zero-value transaction will be sent + + Params `gas_price` and `priority_fee` are Gas Price and Max Priority Fee respectively; + MUST be at least 20% higher than values the original transaction has. + """ + # This one is for legacy transactions + transaction_dict_legacy = { + "nonce": await self.w3.eth.get_transaction_count(self.address) + if custom_nonce is None + else custom_nonce, + "from": _addr_to_str(self.address), + "to": Web3.to_checksum_address(address_to), + "value": Web3.to_wei(0, "wei"), + "gasPrice": Web3.to_wei(gas_price, "gwei"), + "gas": int(self.gas_limit), + "chainId": int(await self.w3.eth.chain_id), + } + signed_txn = self.w3.eth.account.sign_transaction( + transaction_dict_legacy, + self.private_key, + ) + # This one is for post-Merge transactions + transaction_dict = { + "type": 2, + "nonce": await self.w3.eth.get_transaction_count(self.address) + if custom_nonce is None + else custom_nonce, + "from": _addr_to_str(self.address), + "to": _addr_to_str(address_to), + "value": Web3.to_wei(0, "wei"), + "maxFeePerGas": Web3.to_wei(int(gas_price), "gwei"), + "maxPriorityFeePerGas": Web3.to_wei(priority_fee, "gwei"), + "gas": int(self.gas_limit), + "chainId": int(await self.w3.eth.chain_id), + } + signed_txn_london = self.w3.eth.account.sign_transaction( + transaction_dict, + self.private_key, + ) + if self.post_merge: + return await self.w3.eth.send_raw_transaction( + signed_txn_london.raw_transaction + ) + else: + return await self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) + + # Market functions for swapping `qty` amount of `token0` to buy `token1` + async def make_swap_input( + self, + input_token: str, + output_token: str, + qty: int, + qtycap: int, + swap_pool_key: PoolKey | None = None, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param output_token: The token to be bought. + :param qty: The amount of `input_token` to be sold. + :param qtycap: The minimum amount of `output_token` that must be bought for the trade to proceed. + :param swap_pool_key: Optional. The PoolKey for the swap, required if `route` is not provided. + :param hook_data: Optional. The hook data for the swap. This is only used for single hop swaps and will be ignored if `route` is provided. + :param route: Optional. The route for the swap. The first element should be the pool containing `input_token` and the last element should be the pool containing `output_token`. If `route` is provided, it will be used for the swap instead of `swap_pool_key`. + :param custom_nonce: Optional. The nonce for the transaction. + + Make a trade by defining the qty of the input token. + If `route` is provided, it will be used for the swap. Otherwise, `swap_pool_key` must be provided for a single hop swap.""" + result: HexBytes | None = None + if route is None: + if swap_pool_key is None: + raise ValueError("swap_pool_key must be provided for single hop swaps") + result = await self.token_to_token_swap_exact_input( + input_token, + qty, + qtycap, + output_token, + swap_pool_key.fee, + swap_pool_key.tick_spacing, + swap_pool_key.hooks, + hook_data, # type: ignore[arg-type] + min_hop_price_x_36=0, + custom_nonce=custom_nonce, + ) + else: + result = await self.token_to_token_swap_input( + input_token, + qty, + qtycap, + route, + min_hop_price_x_36=[], + custom_nonce=custom_nonce, + ) + return result + + async def make_swap_output( + self, + input_token: str, + output_token: str, + qty: int, + qtycap: int, + swap_pool_key: PoolKey | None = None, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param output_token: The token to be bought. + :param qty: The amount of `output_token` to be bought. + :param qtycap: The maximum amount of `input_token` that can be sold for the trade to proceed. + :param swap_pool_key: Optional. The PoolKey for the swap, required if `route` is not provided. + :param hook_data: Optional. The hook data for the swap. + :param route: Optional. The route for the swap in reverse order. The first element should be the pool containing `output_token` and the last element should be the pool containing `input_token`. + + Make a trade by defining the qty of the output token. + If `route` is provided, it will be used for the swap. Otherwise, `swap_pool_key` must be provided for a single hop swap. + """ + result: HexBytes | None = None + if route is None: + if swap_pool_key is None: + raise ValueError("swap_pool_key must be provided for single hop swaps") + result = await self.token_to_token_swap_exact_output( + input_token, + qty, + qtycap, + output_token, + swap_pool_key.fee, + swap_pool_key.tick_spacing, + swap_pool_key.hooks, + hook_data, # type: ignore[arg-type] + min_hop_price_x_36=0, + custom_nonce=custom_nonce, + ) + else: + result = await self.token_to_token_swap_output( + output_token, + qty, + qtycap, + route, + min_hop_price_x_36=[], + custom_nonce=custom_nonce, + ) + return result + + # Liquidity management functions + async def get_position_info(self, token_id: int) -> dict: + """ + Get information about a liquidity position given its token ID. + :return: A dictionary with the following keys: + + - currency0: The address of the first token in the pool + - currency1: The address of the second token in the pool + - fee: The fee tier of the pool + - tickSpacing: The tick spacing of the pool + - hooks: The hooks address of the pool + - poolID: The truncated pool ID of the position, which is the first 25 bytes of the full pool ID + - tickLower: The lower tick of the position + - tickUpper: The upper tick of the position + - hasSubscriber: A boolean indicating whether the position has a subscriber + - owner: The address of the owner of the position + """ + position_info = await self.position_manager_get_pool_and_position_info(token_id) + + pool_key: PoolKey = position_info["poolKey"] + pool_info = position_info["info"] + pool_info_decoded = self.decode_position_info(pool_info) + owner_of = await self.position_manager_get_owner_of(token_id) + return_value: dict = { + "currency0": pool_key.currency0, + "currency1": pool_key.currency1, + "fee": pool_key.fee, + "tickSpacing": pool_key.tick_spacing, + "hooks": pool_key.hooks, + "poolID": pool_info_decoded["poolID"], + "tickLower": pool_info_decoded["tickLower"], + "tickUpper": pool_info_decoded["tickUpper"], + "hasSubscriber": pool_info_decoded["hasSubscriber"], + "owner": owner_of, + } + return return_value + + async def get_position_value( + self, token_id: int, token0_decimals: int, token1_decimals: int + ) -> dict: + """ + Get the value of a liquidity position given its token ID. + """ + liquidity: int = await self.position_manager_get_position_liquidity(token_id) + position_info = await self.get_position_info(token_id) + slot0 = await self.stateview_get_slot0( + position_info["currency0"], + position_info["currency1"], + position_info["fee"], + position_info["tickSpacing"], + position_info["hooks"], + ) + # price: Decimal = Decimal( + # Decimal( + # decode_sqrt_ratioX96( + # slot0["sqrtPriceX96"], + # ) + # ) + # / Decimal(10 ** (token1_decimals - token0_decimals)) + # ) + sqrt_price = int(slot0["sqrtPriceX96"]) + raw_price: Decimal = (sqrt_price / Decimal(Q96)) ** 2 + decimal_factor: Decimal = 10**token1_decimals / Decimal(10**token0_decimals) + price: Decimal = raw_price / decimal_factor + amounts = self.get_amounts_for_liquidity_by_ticks( + slot0["sqrtPriceX96"], + position_info["tickLower"], + position_info["tickUpper"], + liquidity, + ) + amount0: Decimal = Decimal(amounts["amount0"]) / Decimal(10**token0_decimals) + amount1: Decimal = Decimal(amounts["amount1"]) / Decimal(10**token1_decimals) + return_value: dict = { + "amount0": amounts["amount0"], + "amount1": amounts["amount1"], + # NOTE: unclaimed fees are not yet computed; total values equal principal only + "unclaimed_fees0": 0, + "unclaimed_fees1": 0, + "total_amount0": amounts["amount0"], + "total_amount1": amounts["amount1"], + "value_in_token0": amount0 + amount1 / price, + "value_in_token1": amount1 + amount0 * price, + } + return return_value + + async def create_pool( + self, + pool_key: PoolKey, + sqrt_price_x96: int, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Creates a new liquidity pool without initial liquidity with the specified parameters and a starting price. + """ + function = self.position_manager.functions.initializePool( + astuple(pool_key), + sqrt_price_x96, + ) + tx = await self._build_and_send_tx( + function, + await self._get_tx_params(custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def mint_position( + self, + pool_key: PoolKey, + tick_lower: int, + tick_upper: int, + liquidity: int, + amount0: int, + amount1: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Mints a new liquidity position with the specified parameters. + + :param pool_key: The parameters of the pool for which the position is being minted. + :param tick_lower: The lower tick of the position. + :param tick_upper: The upper tick of the position. + :param liquidity: The amount of liquidity to mint. + :param amount0: The amount of token0 to provide as liquidity. + :param amount1: The amount of token1 to provide as liquidity. + :param recipient: The address that will receive excessive ETH amounts. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the minting. + :return: The transaction hash of the minting transaction. + """ + + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: MINT_POSITION, SETTLE_PAIR, SWEEP, SWEEP + if pool_key.currency0 == ETH_ADDRESS: + ether_amount = amount0 + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + [ + v4_actions["MINT_POSITION"], + v4_actions["SETTLE_PAIR"], + v4_actions["SWEEP"], + v4_actions["SWEEP"], + ], + ) + + # Encoding params + mint_position_params: bytes = encode( + [ + "(address,address,uint24,int24,address)", + "int24", + "int24", + "uint256", + "uint128", + "uint128", + "address", + "bytes", + ], + [ + astuple(pool_key), + tick_lower, + tick_upper, + liquidity, + amount0, + amount1, + recipient, + hook_data, + ], + ) + settle_pair_params: bytes = encode( + ["address", "address"], + [pool_key.currency0, pool_key.currency1], + ) + sweep0_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency0, recipient], + ) + sweep1_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency1, recipient], + ) + params: list[bytes] = [ + mint_position_params, + settle_pair_params, + sweep0_params, + sweep1_params, + ] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = await self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def increase_liquidity( + self, + pool_key: PoolKey, + token_id: int, + amount0_max: int, + amount1_max: int, + liquidity: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Increases the liquidity of an existing position. + + :param pool_key: The parameters of the pool for which the position is being increased. + :param token_id: The token ID of the position to increase liquidity for. + :param amount0_max: The maximum amount of token0 to provide as liquidity. + :param amount1_max: The maximum amount of token1 to provide as liquidity. + :param liquidity: The amount of liquidity to add to the position. + :param recipient: The address that will receive excessive ETH amounts. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the liquidity increase. + :return: The transaction hash of the liquidity increase transaction. + """ + + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: INCREASE_LIQUIDITY, SETTLE_PAIR, SWEEP, SWEEP + if pool_key.currency0 == ETH_ADDRESS: + ether_amount = amount0_max + + actions = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + [ + v4_actions["INCREASE_LIQUIDITY"], + v4_actions["SETTLE_PAIR"], + v4_actions["SWEEP"], + v4_actions["SWEEP"], + ], + ) + # Encoding params + increase_liquidity_params: bytes = encode( + ["uint256", "uint256", "uint128", "uint128", "bytes"], + [ + token_id, + liquidity, + amount0_max, + amount1_max, + hook_data, + ], + ) + settle_pair_params: bytes = encode( + ["address", "address"], + [pool_key.currency0, pool_key.currency1], + ) + sweep0_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency0, recipient], + ) + sweep1_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency1, recipient], + ) + params: list[bytes] = [ + increase_liquidity_params, + settle_pair_params, + sweep0_params, + sweep1_params, + ] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = await self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def decrease_liquidity( + self, + pool_key: PoolKey, + token_id: int, + amount0_min: int, + amount1_min: int, + liquidity: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Decreases the liquidity of an existing position. + + :param pool_key: The parameters of the pool for which the position is being decreased. + :param token_id: The token ID of the position to decrease liquidity for. + :param amount0_min: The minimum amount of token0 to receive from the liquidity decrease. + :param amount1_min: The minimum amount of token1 to receive from the liquidity decrease. + :param liquidity: The amount of liquidity to remove from the position. + :param recipient: The address that will receive the withdrawn liquidity. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the liquidity decrease. + :return: The transaction hash of the liquidity decrease transaction. + """ + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: DECREASE_LIQUIDITY, TAKE_PAIR + actions = encode_packed( + ["uint8", "uint8"], + [ + v4_actions["DECREASE_LIQUIDITY"], + v4_actions["TAKE_PAIR"], + ], + ) + # Encoding params + decrease_liquidity_params: bytes = encode( + ["uint256", "uint256", "uint128", "uint128", "bytes"], + [ + token_id, + liquidity, + amount0_min, + amount1_min, + hook_data, + ], + ) + take_pair_params: bytes = encode( + ["address", "address", "address"], + [pool_key.currency0, pool_key.currency1, recipient], + ) + params: list[bytes] = [decrease_liquidity_params, take_pair_params] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = await self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def collect_fees( + self, + pool_key: PoolKey, + token_id: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Collects the fees accrued by an existing position. + + :param pool_key: The parameters of the pool for which the position is collecting fees. + :param token_id: The token ID of the position for which to collect fees. + :param recipient: The address that will receive the collected fees. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the fee collection. + :return: The transaction hash of the fee collection transaction. + """ + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: DECREASE_LIQUIDITY, TAKE_PAIR + actions = encode_packed( + ["uint8", "uint8"], + [ + v4_actions["DECREASE_LIQUIDITY"], + v4_actions["TAKE_PAIR"], + ], + ) + # Encoding params + decrease_liquidity_params: bytes = encode( + ["uint256", "uint256", "uint128", "uint128", "bytes"], + [ + token_id, + 0, + 0, + 0, + hook_data, + ], + ) + take_pair_params: bytes = encode( + ["address", "address", "address"], + [pool_key.currency0, pool_key.currency1, recipient], + ) + params: list[bytes] = [decrease_liquidity_params, take_pair_params] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = await self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def burn_position( + self, + pool_key: PoolKey, + token_id: int, + amount0_min: int, + amount1_min: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Burns an existing liquidity position. + + :param pool_key: The parameters of the pool for which the position is being burned. + :param token_id: The token ID of the position to burn. + :param amount0_min: The minimum amount of token0 to receive from burning the position. + :param amount1_min: The minimum amount of token1 to receive from burning the position. + :param recipient: The address that will receive the withdrawn liquidity. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the position burn. + :return: The transaction hash of the position burn transaction. + """ + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: BURN_POSITION, TAKE_PAIR + actions = encode_packed( + ["uint8", "uint8"], + [ + v4_actions["BURN_POSITION"], + v4_actions["TAKE_PAIR"], + ], + ) + # Encoding params + burn_position_params: bytes = encode( + ["uint256", "uint128", "uint128", "bytes"], + [ + token_id, + amount0_min, + amount1_min, + hook_data, + ], + ) + take_pair_params: bytes = encode( + ["address", "address", "address"], + [pool_key.currency0, pool_key.currency1, recipient], + ) + params: list[bytes] = [burn_position_params, take_pair_params] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = await self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def universal_router_execute( + self, + commands: list[int], + actions: list[list[int]], + params: list[list[list]], + ether_amount: int = 0, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Executes a transaction with the Universal Router with the specified commands, actions, and parameters. + + :param commands: A list of command integers corresponding to the commands to execute. + :param actions: A list of lists of action integers, where each sublist corresponds to the actions for the respective command in the `commands` list. + :param params: A list of lists of lists of parameters, where each sublist corresponds to the parameters for the respective actions in the `actions` list. The structure of the parameters should match the expected input for each action as defined in the Universal Router documentation. + :param ether_amount: The amount of ether to send with the transaction, if any. + :param custom_nonce: Optional. The nonce for the transaction. + + See ``tests/test_uniswap4.py`` for an example which uses this, `test_universal_router_execute_multiaction()` specific test. + + NOTE: This function does not perform checks on the validity of the commands ``SWAP_EXACT_IN*`` and ``SWAP_EXACT_OUT*``. It is the caller's responsibility to ensure that they are correct and will not cause the transaction to revert. + """ + # Validating input parameters + ignore_list = [ + "SWAP_EXACT_IN_SINGLE", + "SWAP_EXACT_IN", + "SWAP_EXACT_OUT_SINGLE", + "SWAP_EXACT_OUT", + ] + if len(commands) != len(actions) or len(actions) != len(params): + raise ValueError("Lists' lengths are not equal.") + for commands_item, actions_item, params_item in zip(commands, actions, params): + command_key: str = self._get_dict_key_by_value( + universal_router_commands, commands_item + ) + if len(actions_item) == 0: + if len(universal_router_commands_abis[command_key]) != sum( + len(sub_list) for sub_list in params_item + ): + raise ValueError("ABI mismatch for " + command_key + " command.") + else: + for specific_action, specific_param in zip(actions_item, params_item): + action_key: str = self._get_dict_key_by_value( + v4_actions, specific_action + ) + if action_key in ignore_list: + continue + if len(v4_actions_abis[action_key]) != len(specific_param): + raise ValueError( + "ABI mismatch for " + + action_key + + " command in " + + command_key + + " command." + ) + + # Encoding data + commands_abi = ["uint8"] * len(commands) + encoded_commands: bytes = encode_packed(commands_abi, commands) + encoded_inputs: list[bytes] = [] + for commands_item, actions_item, params_item in zip(commands, actions, params): + command_key = self._get_dict_key_by_value( + universal_router_commands, commands_item + ) + if len(actions_item) == 0: + encoded_params: bytes = encode( + universal_router_commands_abis[command_key], params_item[0] + ) + encoded_inputs.append(encoded_params) + else: + encoded_actions_with_params = self.encode_actions_with_params( + actions_item, params_item + ) + encoded_params = encode( + universal_router_commands_abis[command_key], + [ + encoded_actions_with_params["actions"], + encoded_actions_with_params["params"], + ], + ) + encoded_inputs.append(encoded_params) + # Execute contract call + result: HexBytes = await self._build_and_send_tx( + self.router.functions.execute( + encoded_commands, encoded_inputs, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + return result + + # Helper functions + def encode_actions_with_params( + self, actions: list[int], params: list[list] + ) -> dict: + encoded_actions_abi = ["uint8"] * len(actions) + encoded_actions: bytes = encode_packed(encoded_actions_abi, actions) + encoded_params: list[bytes] = [] + for actions_item, params_item in zip(actions, params): + action_key = self._get_dict_key_by_value(v4_actions, actions_item) + encoded_params_item: bytes = encode( + v4_actions_abis[action_key], params_item + ) + encoded_params.append(encoded_params_item) + return_value: dict = { + "actions": encoded_actions, + "params": encoded_params, + } + return return_value + + @staticmethod + def _get_dict_key_by_value(param_dict: dict, value: int) -> str: + return_value = next((k for k, v in param_dict.items() if v == value), None) + if return_value is None: + raise IndexError("Key is not found.") + return str(return_value) + + @staticmethod + def get_liquidity_for_amount0( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, amount0: int + ) -> int: + """ + Helper function to calculate the amount of liquidity that can be provided for a given amount of `token0` and price range defined by `sqrt_ ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + liquidity: int = (amount0 * (sqrt_ratio_a_x96 * sqrt_ratio_b_x96 // Q96)) // ( + sqrt_ratio_b_x96 - sqrt_ratio_a_x96 + ) + return liquidity + + @staticmethod + def get_liquidity_for_amount1( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, amount1: int + ) -> int: + """ + Helper function to calculate the amount of liquidity that can be provided for a given amount of `token1` and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + liquidity: int = (amount1 * Q96) // (sqrt_ratio_b_x96 - sqrt_ratio_a_x96) + return liquidity + + def get_liquidity_for_amounts( + self, + sqrt_ratio_a_x96: int, + sqrt_ratio_b_x96: int, + sqrt_ratio_current_x96: int, + amount0: int, + amount1: int, + ) -> int: + """ + Helper function to calculate the amount of liquidity that can be provided for given amounts of `token0` and `token1` and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + if sqrt_ratio_current_x96 <= sqrt_ratio_a_x96: + liquidity: int = self.get_liquidity_for_amount0( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, amount0 + ) + elif sqrt_ratio_current_x96 < sqrt_ratio_b_x96: + liquidity0: int = self.get_liquidity_for_amount0( + sqrt_ratio_current_x96, sqrt_ratio_b_x96, amount0 + ) + liquidity1: int = self.get_liquidity_for_amount1( + sqrt_ratio_a_x96, sqrt_ratio_current_x96, amount1 + ) + liquidity = min(liquidity0, liquidity1) + else: + liquidity = self.get_liquidity_for_amount1( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, amount1 + ) + return liquidity + + @staticmethod + def get_amount0_for_liquidity( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, liquidity: int + ) -> int: + """ + Helper function to calculate the amount of `token0` that can be provided for a given amount of liquidity and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + amount0: int = ( + (liquidity * Q96 * (sqrt_ratio_b_x96 - sqrt_ratio_a_x96)) + // sqrt_ratio_b_x96 + // sqrt_ratio_a_x96 + ) + return amount0 + + @staticmethod + def get_amount1_for_liquidity( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, liquidity: int + ) -> int: + """ + Helper function to calculate the amount of `token1` that can be provided for a given amount of liquidity and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + amount1: int = (liquidity * (sqrt_ratio_b_x96 - sqrt_ratio_a_x96)) // Q96 + return amount1 + + def get_amounts_for_liquidity( + self, + sqrt_ratio_a_x96: int, + sqrt_ratio_b_x96: int, + sqrt_ratio_current_x96: int, + liquidity: int, + ) -> dict: + """ + Helper function to calculate the amounts of `token0` and `token1` that can be provided for a given amount of liquidity and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + amount0: int = 0 + amount1: int = 0 + if sqrt_ratio_current_x96 <= sqrt_ratio_a_x96: + amount0 = self.get_amount0_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, liquidity + ) + elif sqrt_ratio_current_x96 < sqrt_ratio_b_x96: + amount0 = self.get_amount0_for_liquidity( + sqrt_ratio_current_x96, sqrt_ratio_b_x96, liquidity + ) + amount1 = self.get_amount1_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_current_x96, liquidity + ) + else: + amount1 = self.get_amount1_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, liquidity + ) + return_value: dict = { + "amount0": amount0, + "amount1": amount1, + } + return return_value + + def get_amounts_for_liquidity_by_ticks( + self, ratio_current_x96: int, tick_lower: int, tick_upper: int, liquidity: int + ) -> dict: + sqrt_ratio_a_x96 = get_sqrt_ratio_at_tick(tick_lower) + sqrt_ratio_b_x96 = get_sqrt_ratio_at_tick(tick_upper) + return_value: dict = self.get_amounts_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, ratio_current_x96, liquidity + ) + return return_value + + def get_liquidity_for_amounts_by_ticks( + self, + ratio_current_x96: int, + tick_lower: int, + tick_upper: int, + amount0: int, + amount1: int, + ) -> int: + sqrt_ratio_a_x96 = get_sqrt_ratio_at_tick(tick_lower) + sqrt_ratio_b_x96 = get_sqrt_ratio_at_tick(tick_upper) + liquidity = self.get_liquidity_for_amounts( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, ratio_current_x96, amount0, amount1 + ) + return liquidity + + async def get_minted_token_id(self, tx_hash: str) -> list[int]: + """ + Helper function to extract the token ID of a newly minted position from the transaction receipt of the minting transaction. + + :return: A list of token IDs of the newly minted positions; empty list if none can be extracted. In most cases, this list will contain only one token ID, but in some cases (e.g., if multiple positions are minted in a single transaction), it may contain multiple token IDs. + """ + transaction_receipt = await self.w3.eth.get_transaction_receipt(tx_hash) # type: ignore [arg-type] + logs = self.position_manager.events.Transfer().process_receipt( + transaction_receipt + ) + return_value: list[int] = [] + for log in logs: + try: + if _addr_to_str(log.args["from"]) == ZERO_HOOK: + minted_token_id: int = log.args.id + return_value.append(minted_token_id) + except (AttributeError, KeyError): + logger.warning( + "Could not extract minted token ID from transaction receipt for transaction hash: %s.", + tx_hash, + ) + return return_value + + @staticmethod + def decode_position_info(position_info: int) -> dict: + """ + + :return: + A dictionary with the following keys: + - `tickLower`: The lower tick of the position. + - `tickUpper`: The upper tick of the position. + - `poolID`: The truncated pool ID of the position, which is the first 25 bytes of the full pool ID. + - `hasSubscriber`: A boolean indicating whether the position has a subscriber. + """ + tick_lower_offset = 8 + tick_upper_offset = 32 + pool_id_offset = 56 + sign_bit = 0x800000 + + # The hasSubscriber flag is stored in the least significant byte of the position info, so we can use a bitwise AND operation with a mask of 0xFF to extract it. + mask = 0xFF + has_subscriber: bool = (position_info & mask) != 0 + + # The tick lower and tick upper values are stored in the next 3 bytes each, so we shift the position info to the right by the respective offsets and use a bitwise AND operation with a mask of 0xFFFFFF to extract them. We also check if the sign bit is set to determine if the tick values are negative, and if so, we subtract 0x1000000 from them to get the correct negative value. + mask = 0xFFFFFF + tick_lower: int = (position_info >> tick_lower_offset) & mask + if tick_lower & sign_bit: + tick_lower = tick_lower - 0x1000000 + + tick_upper: int = (position_info >> tick_upper_offset) & mask + if tick_upper & sign_bit: + tick_upper = tick_upper - 0x1000000 + + # The pool ID is stored in the remaining bytes, so we shift the position info to the right by the pool ID offset to get the pool ID. + rest_part: int = position_info >> pool_id_offset + pool_id: bytes = rest_part.to_bytes(25, byteorder="big") + + return_value: dict = { + "tickLower": tick_lower, + "tickUpper": tick_upper, + "poolID": pool_id, + "hasSubscriber": has_subscriber, + } + return return_value + + @staticmethod + def encode_path_keys_input( + path: list[PoolKey], + currency_in: str, + hook_data_list: list[bytes] | None = None, + ) -> list[PathKey]: + """ + Encodes a list of PoolKeys into the format expected by the quoter for multi-hop ExactInput quotes. + """ + encoded_path: list[PathKey] = [] + if hook_data_list is None: + hook_data_list = [b""] * len(path) + else: + if len(hook_data_list) != len(path): + raise ValueError("Length of hook_data_list must match length of path") + for pool_key, hook_data in zip(path, hook_data_list): + currency_out: str = ( + pool_key.currency1 + if currency_in.lower() == pool_key.currency0.lower() + else pool_key.currency0 + ) + path_key: PathKey = PathKey( + currency_out, + pool_key.fee, + pool_key.tick_spacing, + pool_key.hooks, + hook_data, + ) + encoded_path.append(path_key) + currency_in = currency_out + return encoded_path + + @staticmethod + def encode_path_keys_output( + path: list[PoolKey], + currency_out: str, + hook_data_list: list[bytes] | None = None, + ) -> list[PathKey]: + """ + Encodes a list of PoolKeys into the format expected by the quoter for multi-hop ExactOutput quotes. + + """ + encoded_path: list[PathKey] = [] + if hook_data_list is None: + hook_data_list = [b""] * len(path) + else: + if len(hook_data_list) != len(path): + raise ValueError("Length of hook_data_list must match length of path") + for pool_key, hook_data in zip(reversed(path), reversed(hook_data_list)): + currency_in: str = ( + pool_key.currency1 + if currency_out.lower() == pool_key.currency0.lower() + else pool_key.currency0 + ) + path_key: PathKey = PathKey( + currency_in, + pool_key.fee, + pool_key.tick_spacing, + pool_key.hooks, + hook_data, + ) + encoded_path.insert(0, path_key) + currency_out = currency_in + return encoded_path + + @staticmethod + def get_pool_id(pool: PoolKey) -> HexBytes: + """Computes the pool ID for a given PoolKey by hashing its parameters.""" + pool_data = eth_abi.abi.encode( + types=["address", "address", "uint24", "int24", "address"], + args=[ + pool.currency0, + pool.currency1, + pool.fee, + pool.tick_spacing, + pool.hooks, + ], + ) + pool_id = Web3.keccak(pool_data) + return pool_id + + async def get_token( + self, address: AddressLike, abi_name: str = "erc20" + ) -> ERC20Token: + """ + Retrieves metadata from the ERC20 contract of a given token, like its name, symbol, and decimals. + """ + if address == ETH_ADDRESS or address == _str_to_addr(ETH_ADDRESS): + return ERC20Token( + address=address, + name="ETH", + symbol="ETH", + decimals=18, + ) + token_contract = _load_contract(self.w3, abi_name, address=address) + try: + _name = await token_contract.functions.name().call() + _symbol = await token_contract.functions.symbol().call() + decimals = await token_contract.functions.decimals().call() + except ValueError: + raise InvalidToken(address) + try: + name = await _name.decode() + except (ValueError, AttributeError): + name = str(_name) + try: + symbol = await _symbol.decode() + except (ValueError, AttributeError) as e: + logger.warning( + "Error occurred while decoding symbol for %s: %s", + _addr_to_str(address), + e, + ) + symbol = str(_symbol) + return ERC20Token(symbol, address, name, decimals) + + async def get_token_balance(self, erc20: AddressLike) -> Decimal: + """Get the balance of an ERC20 token for your address.""" + contract = _load_contract(self.w3, abi_name="erc20", address=erc20) + decimals: int = await contract.functions.decimals().call() + balance: int = await contract.functions.balanceOf(self.address).call() + return_balance: Decimal = Decimal(balance) / Decimal(10**decimals) + return return_balance + + async def get_balance(self) -> Decimal: + """Get the balance of ETH for your address.""" + balance: int = await self.w3.eth.get_balance(self.address) + return_balance: Decimal = Decimal(balance) / Decimal(10**18) + return return_balance + + def load_contract_with_abi( + self, abi_name: str, address: AddressLike + ) -> AsyncContract: + return self.w3.eth.contract(address=address, abi=_load_abi(abi_name)) + + def erc20_contract(self, token_addr: AddressLike) -> AsyncContract: + return self.load_contract_with_abi(abi_name="erc20", address=token_addr) + + def _deadline(self) -> int: + """Get a predefined deadline. 10min by default.""" + return int(time.time()) + 10 * 60 + + async def _get_tx_params( + self, value: int = 0, custom_nonce: Nonce | None = None + ) -> TxParams: + """Get generic transaction parameters.""" + if custom_nonce is not None and custom_nonce < Nonce(0): + raise ValueError("Nonce can only be a positive integer.") + if not self.post_merge: + return { + "from": _addr_to_str(self.address), + "value": Wei(value), + "gas": int(self.gas_limit), + "gasPrice": Web3.to_wei(self.gas_price, "gwei"), + "nonce": Nonce(max(self.last_nonce, 0)) + if custom_nonce is None + else custom_nonce, + } + else: + return { + "from": _addr_to_str(self.address), + "gas": int(self.gas_limit), + "maxPriorityFeePerGas": Web3.to_wei(self.priority_fee, "gwei"), + "maxFeePerGas": Web3.to_wei(self.gas_price, "gwei"), + "type": 2, + "chainId": await self.w3.eth.chain_id, + "value": Wei(value), + "nonce": Nonce(max(self.last_nonce, 0)) + if custom_nonce is None + else custom_nonce, + } + + async def _build_and_send_tx( + self, + function: AsyncContractFunction, + tx_params: TxParams | None = None, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """Build and send a transaction.""" + if not tx_params: + tx_params = await self._get_tx_params(custom_nonce=custom_nonce) + transaction = await function.build_transaction(tx_params) + signed_txn = self.w3.eth.account.sign_transaction( + transaction, private_key=self.private_key + ) + try: + return await self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) + finally: + # logger.debug(f"nonce: {tx_params['nonce']}") + if custom_nonce is None: + self.last_nonce = Nonce(tx_params["nonce"] + 1) + + +def _load_contract(w3: AsyncWeb3, abi_name: str, address: AddressLike) -> AsyncContract: + address = w3.to_checksum_address(address) + return w3.eth.contract(address=address, abi=_load_abi(abi_name)) + + +def _load_contract_erc20(w3: AsyncWeb3, address: AddressLike) -> AsyncContract: + return _load_contract(w3, "erc20", address) diff --git a/uniswap/cli.py b/uniswap/cli.py index 78302cf5..8ba54e2f 100644 --- a/uniswap/cli.py +++ b/uniswap/cli.py @@ -1,6 +1,5 @@ import logging import os -from typing import Optional import click from dotenv import load_dotenv @@ -15,7 +14,7 @@ logger = logging.getLogger(__name__) # Global used in _coerce_to_checksum to look up tokens -_uni: Optional[Uniswap] = None +_uni: Uniswap | None = None def _coerce_to_checksum(addr: str) -> str: @@ -71,7 +70,7 @@ def price( token_in: AddressLike, token_out: AddressLike, raw: bool, - quantity: Optional[int] = None, + quantity: int | None = None, ) -> None: """Returns the price of ``quantity`` tokens of ``token_in`` quoted in ``token_out``.""" uni: Uniswap = ctx.obj["UNISWAP"] @@ -81,7 +80,9 @@ def price( else: decimals = uni.get_token(token_in).decimals quantity = 10**decimals - price = uni.get_price_input(token_in, token_out, qty=quantity, fee=FeeTier.TIER_3000) + price = uni.get_price_input( + token_in, token_out, qty=quantity, fee=FeeTier.TIER_3000 + ) if raw: click.echo(price) else: diff --git a/uniswap/constants.py b/uniswap/constants.py index b9459676..f7d15db0 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -1,11 +1,11 @@ -from typing import Set, cast +from typing import cast -from web3.types import RPCEndpoint # noqa: F401 +from web3.types import RPCEndpoint # look at web3/middleware/cache.py for reference # RPC methods that will be cached inside _get_eth_simple_cache_middleware SIMPLE_CACHE_RPC_WHITELIST = cast( - Set[RPCEndpoint], + set[RPCEndpoint], { "eth_chainId", }, diff --git a/uniswap/decorators.py b/uniswap/decorators.py index dfced8f6..d886028f 100644 --- a/uniswap/decorators.py +++ b/uniswap/decorators.py @@ -1,14 +1,12 @@ import functools +from collections.abc import Callable from typing import ( TYPE_CHECKING, - Callable, - List, - Optional, + Concatenate, + ParamSpec, TypeVar, ) -from typing_extensions import Concatenate, ParamSpec - from .constants import ETH_ADDRESS from .types import AddressLike @@ -21,7 +19,7 @@ def check_approval( - method: Callable[Concatenate["Uniswap", P], T] + method: Callable[Concatenate["Uniswap", P], T], ) -> Callable[Concatenate["Uniswap", P], T]: """Decorator to check if user is approved for a token. It approves them if they need to be approved.""" @@ -29,12 +27,12 @@ def check_approval( @functools.wraps(method) def approved(self: "Uniswap", *args: P.args, **kwargs: P.kwargs) -> T: # Check to see if the first token is actually ETH - token: Optional[AddressLike] = args[0] if args[0] != ETH_ADDRESS else None # type: ignore - token_two = None + token: AddressLike | None = args[0] if args[0] != ETH_ADDRESS else None # type: ignore + _token_two = None # Check second token, if needed if method.__name__ == "make_trade" or method.__name__ == "make_trade_output": - token_two = args[1] if args[1] != ETH_ADDRESS else None + _token_two = args[1] if args[1] != ETH_ADDRESS else None # Approve both tokens, if needed if token: @@ -48,25 +46,23 @@ def approved(self: "Uniswap", *args: P.args, **kwargs: P.kwargs) -> T: def supports( - versions: List[int], + versions: list[int], ) -> Callable[ [Callable[Concatenate["Uniswap", P], T]], Callable[Concatenate["Uniswap", P], T] ]: def g( - f: Callable[Concatenate["Uniswap", P], T] + f: Callable[Concatenate["Uniswap", P], T], ) -> Callable[Concatenate["Uniswap", P], T]: if f.__doc__ is None: f.__doc__ = "" f.__doc__ += """\n\n Supports Uniswap - """ + ", ".join( - "v" + str(ver) for ver in versions - ) + """ + ", ".join("v" + str(ver) for ver in versions) @functools.wraps(f) def check_version(self: "Uniswap", *args: P.args, **kwargs: P.kwargs) -> T: if self.version not in versions: - raise Exception( + raise ValueError( f"Function {f.__name__} does not support version {self.version} of Uniswap passed to constructor" ) return f(self, *args, **kwargs) diff --git a/uniswap/fee.py b/uniswap/fee.py index 0005d843..91ae3c0b 100644 --- a/uniswap/fee.py +++ b/uniswap/fee.py @@ -1,6 +1,6 @@ -import enum +import enum import logging -from typing import final, Final, Optional +from typing import Final, final from .exceptions import InvalidFeeTier @@ -26,7 +26,7 @@ class FeeTier(enum.IntEnum): TIER_10000 = 10000 -def validate_fee_tier(fee: Optional[int], version: int) -> int: +def validate_fee_tier(fee: int | None, version: int) -> int: """ Validate fee tier for a given Uniswap version. """ diff --git a/uniswap/tokens.py b/uniswap/tokens.py index 6008e644..92edc63e 100644 --- a/uniswap/tokens.py +++ b/uniswap/tokens.py @@ -1,9 +1,7 @@ -from typing import Dict - from eth_typing.evm import ChecksumAddress from web3 import Web3 -tokens_mainnet: Dict[str, ChecksumAddress] = { +tokens_mainnet: dict[str, ChecksumAddress] = { k: Web3.to_checksum_address(v) for k, v in { "ETH": "0x0000000000000000000000000000000000000000", @@ -16,7 +14,7 @@ }.items() } -tokens_rinkeby: Dict[str, ChecksumAddress] = { +tokens_rinkeby: dict[str, ChecksumAddress] = { k: Web3.to_checksum_address(v) for k, v in { "ETH": "0x0000000000000000000000000000000000000000", @@ -25,7 +23,7 @@ }.items() } -tokens_arbitrum: Dict[str, ChecksumAddress] = { +tokens_arbitrum: dict[str, ChecksumAddress] = { k: Web3.to_checksum_address(v) for k, v in { "ETH": "0x0000000000000000000000000000000000000000", @@ -37,7 +35,7 @@ } -def get_tokens(netname: str) -> Dict[str, ChecksumAddress]: +def get_tokens(netname: str) -> dict[str, ChecksumAddress]: """ Returns a dict with addresses for tokens for the current net. Used in testing. @@ -49,4 +47,4 @@ def get_tokens(netname: str) -> Dict[str, ChecksumAddress]: elif netname == "arbitrum": return tokens_arbitrum else: - raise Exception(f"Unknown net '{netname}'") + raise ValueError(f"Unknown net '{netname}'") diff --git a/uniswap/types.py b/uniswap/types.py index f9f90b22..659a4bf4 100644 --- a/uniswap/types.py +++ b/uniswap/types.py @@ -1,9 +1,8 @@ from dataclasses import dataclass -from typing import List, Union from eth_typing.evm import Address, ChecksumAddress -AddressLike = Union[Address, ChecksumAddress] +AddressLike = Address | ChecksumAddress # Uniswap V4 classes @@ -42,7 +41,7 @@ class PermitSingle: @dataclass class PermitBatch: - details: List[PermitDetails] + details: list[PermitDetails] spender: str sig_deadline: int diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 183c349b..79747501 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1,19 +1,13 @@ -import functools import logging import os import time from collections import namedtuple +from collections.abc import Iterable, Sequence from typing import ( Any, - Dict, - Iterable, - List, - Optional, - Sequence, - Tuple, - Union, ) +from eth_typing import HexStr from eth_typing.evm import Address, ChecksumAddress from hexbytes import HexBytes from web3 import Web3 @@ -42,14 +36,13 @@ _tick_bitmap_range, _tick_spacing, ) -from .decorators import check_approval, supports +from .decorators import supports from .exceptions import InsufficientBalance, InvalidToken from .fee import validate_fee_tier from .token import ERC20Token from .types import AddressLike from .util import ( _addr_to_str, - _get_eth_simple_cache_middleware, _load_contract, _load_contract_erc20, _str_to_addr, @@ -81,16 +74,16 @@ class Uniswap: def __init__( self, - address: Union[AddressLike, str, None], - private_key: Optional[str], - provider: Optional[str] = None, - web3: Optional[Web3] = None, + address: AddressLike | str | None, + private_key: str | None, + provider: str | None = None, + web3: Web3 | None = None, version: int = 1, default_slippage: float = 0.01, use_estimate_gas: bool = True, # use_eip1559: bool = True, - factory_contract_addr: Optional[str] = None, - router_contract_addr: Optional[str] = None, + factory_contract_addr: str | None = None, + router_contract_addr: str | None = None, enable_caching: bool = False, ) -> None: """ @@ -114,11 +107,10 @@ def __init__( self.version = version if self.version not in [1, 2, 3]: - raise Exception( + raise ValueError( f"Invalid version '{self.version}', only 1, 2 or 3 supported" ) # pragma: no cover - # TODO: Write tests for slippage self.default_slippage = default_slippage self.use_estimate_gas = use_estimate_gas @@ -130,14 +122,11 @@ def __init__( provider = os.environ["PROVIDER"] self.w3 = Web3(Web3.HTTPProvider(provider, request_kwargs={"timeout": 60})) - if enable_caching: - self.w3.middleware_onion.inject(_get_eth_simple_cache_middleware(), layer=0) - self.netid = int(self.w3.net.version) if self.netid in _netid_to_name: self.netname = _netid_to_name[self.netid] else: - raise Exception(f"Unknown netid: {self.netid}") # pragma: no cover + raise ValueError(f"Unknown netid: {self.netid}") # pragma: no cover logger.info(f"Using {self.w3} ('{self.netname}', netid: {self.netid})") self.last_nonce: Nonce = self.w3.eth.get_transaction_count(self.address) @@ -217,7 +206,7 @@ def __init__( self.w3, abi_name="uniswap-v3/multicall", address=multicall2_addr ) else: - raise Exception( + raise ValueError( f"Invalid version '{self.version}', only 1, 2 or 3 supported" ) @@ -231,36 +220,50 @@ def get_price_input( token0: AddressLike, # input token token1: AddressLike, # output token qty: int, - fee: Optional[int] = None, - route: Optional[List[AddressLike]] = None, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> int: """Given `qty` amount of the input `token0`, returns the maximum output amount of output `token1`.""" fee = validate_fee_tier(fee=fee, version=self.version) - if token0 == ETH_ADDRESS: + if is_same_address(token0, ETH_ADDRESS) and self.version == 2: return self._get_eth_token_input_price(token1, Wei(qty), fee) - elif token1 == ETH_ADDRESS: + elif is_same_address(token1, ETH_ADDRESS) and self.version == 2: return self._get_token_eth_input_price(token0, qty, fee) else: - return self._get_token_token_input_price(token0, token1, qty, fee, route) + if is_same_address(token0, ETH_ADDRESS): + token0 = self.get_weth_address() + if is_same_address(token1, ETH_ADDRESS): + token1 = self.get_weth_address() + return self._get_token_token_input_price( + token0, token1, qty, fee, route, fees + ) def get_price_output( self, token0: AddressLike, token1: AddressLike, qty: int, - fee: Optional[int] = None, - route: Optional[List[AddressLike]] = None, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> int: """Returns the minimum amount of `token0` required to buy `qty` amount of `token1`.""" fee = validate_fee_tier(fee=fee, version=self.version) - if is_same_address(token0, ETH_ADDRESS): + if is_same_address(token0, ETH_ADDRESS) and self.version == 2: return self._get_eth_token_output_price(token1, qty, fee) - elif is_same_address(token1, ETH_ADDRESS): + elif is_same_address(token1, ETH_ADDRESS) and self.version == 2: return self._get_token_eth_output_price(token0, Wei(qty), fee) else: - return self._get_token_token_output_price(token0, token1, qty, fee, route) + if is_same_address(token0, ETH_ADDRESS): + token0 = self.get_weth_address() + if is_same_address(token1, ETH_ADDRESS): + token1 = self.get_weth_address() + return self._get_token_token_output_price( + token0, token1, qty, fee, route, fees + ) def _get_eth_token_input_price( self, @@ -312,38 +315,41 @@ def _get_token_token_input_price( token1: AddressLike, # output token qty: int, fee: int, - route: Optional[List[AddressLike]] = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> int: """ Public price (i.e. amount of output token received) for token to token trades with an exact input. :param fee: (v3 only) The pool's fee in hundredths of a bip, i.e. 1e-6 (3000 is 0.3%) """ - if route is None: - if self.version == 2: - # If one of the tokens are WETH, delegate to appropriate call. - # See: https://github.com/shanefontaine/uniswap-python/issues/22 - if is_same_address(token0, self.get_weth_address()): - return int(self._get_eth_token_input_price(token1, Wei(qty), fee)) - elif is_same_address(token1, self.get_weth_address()): - return int(self._get_token_eth_input_price(token0, qty, fee)) - - route = [token0, self.get_weth_address(), token1] - logger.warning(f"No route specified, assuming route: {route}") + if route is None and self.version == 2: + # If one of the tokens are WETH, delegate to appropriate call. + # See: https://github.com/shanefontaine/uniswap-python/issues/22 + if is_same_address(token0, self.get_weth_address()): + return int(self._get_eth_token_input_price(token1, Wei(qty), fee)) + elif is_same_address(token1, self.get_weth_address()): + return int(self._get_token_eth_input_price(token0, qty, fee)) + + route = [token0, self.get_weth_address(), token1] + logger.warning(f"No route specified, assuming route: {route}") if self.version == 2: price: int = self.router.functions.getAmountsOut(qty, route).call()[-1] elif self.version == 3: - if route: + if route and fees: # NOTE: to support custom routes we need to support the Path data encoding: https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/libraries/Path.sol # result: tuple = self.quoter.functions.quoteExactInput(route, qty).call() - raise Exception("custom route not yet supported for v3") - - # FIXME: How to calculate this properly? See https://docs.uniswap.org/reference/libraries/SqrtPriceMath - sqrtPriceLimitX96 = 0 - price = self.quoter.functions.quoteExactInputSingle( - token0, token1, fee, qty, sqrtPriceLimitX96 - ).call() + encoded_v3_path = self.encode_v3_path(route, fees) + price = self.quoter.functions.quoteExactInput( + encoded_v3_path, qty + ).call() + else: + # FIXME: How to calculate this properly? See https://docs.uniswap.org/reference/libraries/SqrtPriceMath + sqrtPriceLimitX96 = 0 + price = self.quoter.functions.quoteExactInputSingle( + token0, token1, fee, qty, sqrtPriceLimitX96 + ).call() else: raise ValueError("function not supported for this version of Uniswap") return price @@ -352,7 +358,7 @@ def _get_eth_token_output_price( self, token: AddressLike, # output token qty: int, - fee: Optional[int] = None, + fee: int | None = None, ) -> Wei: """Public price (i.e. amount of ETH needed) for ETH to token trades with an exact output.""" fee = validate_fee_tier(fee=fee, version=self.version) @@ -373,7 +379,10 @@ def _get_eth_token_output_price( return price def _get_token_eth_output_price( - self, token: AddressLike, qty: Wei, fee: Optional[int] = None # input token + self, + token: AddressLike, + qty: Wei, + fee: int | None = None, # input token ) -> int: """Public price (i.e. amount of input token needed) for token to ETH trades with an exact output.""" fee = validate_fee_tier(fee=fee, version=self.version) @@ -397,8 +406,9 @@ def _get_token_token_output_price( token0: AddressLike, # input token token1: AddressLike, # output token qty: int, - fee: Optional[int] = None, - route: Optional[List[AddressLike]] = None, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> int: """ Public price (i.e. amount of input token needed) for token to token trades with an exact output. @@ -406,48 +416,51 @@ def _get_token_token_output_price( :param fee: (v3 only) The pool's fee in hundredths of a bip, i.e. 1e-6 (3000 is 0.3%) """ fee = validate_fee_tier(fee=fee, version=self.version) - if not route: - if self.version == 2: - # If one of the tokens are WETH, delegate to appropriate call. - # See: https://github.com/shanefontaine/uniswap-python/issues/22 - if is_same_address(token0, self.get_weth_address()): - return int(self._get_eth_token_output_price(token1, qty, fee)) - elif is_same_address(token1, self.get_weth_address()): - return int(self._get_token_eth_output_price(token0, Wei(qty), fee)) - - route = [token0, self.get_weth_address(), token1] - logger.warning(f"No route specified, assuming route: {route}") + if not route and self.version == 2: + # If one of the tokens are WETH, delegate to appropriate call. + # See: https://github.com/shanefontaine/uniswap-python/issues/22 + if is_same_address(token0, self.get_weth_address()): + return int(self._get_eth_token_output_price(token1, qty, fee)) + elif is_same_address(token1, self.get_weth_address()): + return int(self._get_token_eth_output_price(token0, Wei(qty), fee)) + + route = [token0, self.get_weth_address(), token1] + logger.warning(f"No route specified, assuming route: {route}") if self.version == 2: price: int = self.router.functions.getAmountsIn(qty, route).call()[0] elif self.version == 3: - if route: + if route and fees: # NOTE: to support custom routes we need to support the Path data encoding: https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/libraries/Path.sol # result: tuple = self.quoter.functions.quoteExactOutput(route, qty).call() - raise Exception("custom route not yet supported for v3") - - # FIXME: How to calculate this properly? - # - https://docs.uniswap.org/reference/libraries/SqrtPriceMath - # - https://github.com/Uniswap/uniswap-v3-sdk/blob/main/src/swapRouter.ts - sqrtPriceLimitX96 = 0 - price = self.quoter.functions.quoteExactOutputSingle( - token0, token1, fee, qty, sqrtPriceLimitX96 - ).call() + encoded_v3_path = self.encode_v3_path(route, fees, is_exact_out=True) + price = self.quoter.functions.quoteExactOutput( + encoded_v3_path, qty + ).call() + else: + # FIXME: How to calculate this properly? + # - https://docs.uniswap.org/reference/libraries/SqrtPriceMath + # - https://github.com/Uniswap/uniswap-v3-sdk/blob/main/src/swapRouter.ts + sqrtPriceLimitX96 = 0 + price = self.quoter.functions.quoteExactOutputSingle( + token0, token1, fee, qty, sqrtPriceLimitX96 + ).call() else: raise ValueError # pragma: no cover return price # ------ Make Trade ---------------------------------------------------------------- - @check_approval def make_trade( self, input_token: AddressLike, output_token: AddressLike, - qty: Union[int, Wei], - recipient: Optional[AddressLike] = None, - fee: Optional[int] = None, - slippage: Optional[float] = None, + qty: int | Wei, + recipient: AddressLike | None = None, + fee: int | None = None, + slippage: float | None = None, fee_on_transfer: bool = False, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> HexBytes: """Make a trade by defining the qty of the input token.""" if not isinstance(qty, int): @@ -461,15 +474,22 @@ def make_trade( if input_token == output_token: raise ValueError - if input_token == ETH_ADDRESS: + if self.version != 3 and fees: + fees = None + + if input_token == ETH_ADDRESS and (route is None or fees is None): return self._eth_to_token_swap_input( output_token, Wei(qty), recipient, fee, slippage, fee_on_transfer ) - elif output_token == ETH_ADDRESS: + elif output_token == ETH_ADDRESS and (route is None or fees is None): return self._token_to_eth_swap_input( input_token, qty, recipient, fee, slippage, fee_on_transfer ) else: + # if is_same_address(input_token, ETH_ADDRESS): + # input_token = self.get_weth_address() + # if is_same_address(output_token, ETH_ADDRESS): + # output_token = self.get_weth_address() return self._token_to_token_swap_input( input_token, output_token, @@ -478,17 +498,20 @@ def make_trade( fee, slippage, fee_on_transfer, + route, + fees, ) - @check_approval def make_trade_output( self, input_token: AddressLike, output_token: AddressLike, - qty: Union[int, Wei], - recipient: Optional[AddressLike] = None, - fee: Optional[int] = None, - slippage: Optional[float] = None, + qty: int | Wei, + recipient: AddressLike | None = None, + fee: int | None = None, + slippage: float | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> HexBytes: """Make a trade by defining the qty of the output token.""" fee = validate_fee_tier(fee=fee, version=self.version) @@ -499,7 +522,10 @@ def make_trade_output( if input_token == output_token: raise ValueError - if input_token == ETH_ADDRESS: + if self.version != 3 and fees: + fees = None + + if input_token == ETH_ADDRESS and (route is None or fees is None): balance = self.get_eth_balance() need = self._get_eth_token_output_price(output_token, qty, fee) if balance < need: @@ -507,20 +533,24 @@ def make_trade_output( return self._eth_to_token_swap_output( output_token, qty, recipient, fee, slippage ) - elif output_token == ETH_ADDRESS: + elif output_token == ETH_ADDRESS and (route is None or fees is None): return self._token_to_eth_swap_output( input_token, Wei(qty), recipient, fee, slippage ) else: + # if is_same_address(input_token, ETH_ADDRESS): + # input_token = self.get_weth_address() + # if is_same_address(output_token, ETH_ADDRESS): + # output_token = self.get_weth_address() return self._token_to_token_swap_output( - input_token, output_token, qty, recipient, fee, slippage + input_token, output_token, qty, recipient, fee, slippage, route, fees ) def _eth_to_token_swap_input( self, output_token: AddressLike, qty: Wei, - recipient: Optional[AddressLike], + recipient: AddressLike | None, fee: int, slippage: float, fee_on_transfer: bool = False, @@ -536,7 +566,7 @@ def _eth_to_token_swap_input( if self.version == 1: token_funcs = self._exchange_contract(output_token).functions tx_params = self._get_tx_params(qty) - func_params: List[Any] = [qty, self._deadline()] + func_params: list[Any] = [qty, self._deadline()] if not recipient: function = token_funcs.ethToTokenSwapInput(*func_params) else: @@ -551,9 +581,7 @@ def _eth_to_token_swap_input( (1 - slippage) * self._get_eth_token_input_price(output_token, qty, fee) ) if fee_on_transfer: - func = ( - self.router.functions.swapExactETHForTokensSupportingFeeOnTransferTokens - ) + func = self.router.functions.swapExactETHForTokensSupportingFeeOnTransferTokens else: func = self.router.functions.swapExactETHForTokens return self._build_and_send_tx( @@ -570,7 +598,7 @@ def _eth_to_token_swap_input( recipient = self.address if fee_on_transfer: - raise Exception("fee on transfer not supported by Uniswap v3") + raise ValueError("fee on transfer not supported by Uniswap v3") min_tokens_bought = int( (1 - slippage) @@ -600,7 +628,7 @@ def _token_to_eth_swap_input( self, input_token: AddressLike, qty: int, - recipient: Optional[AddressLike], + recipient: AddressLike | None, fee: int, slippage: float, fee_on_transfer: bool = False, @@ -616,7 +644,7 @@ def _token_to_eth_swap_input( if self.version == 1: token_funcs = self._exchange_contract(input_token).functions - func_params: List[Any] = [qty, 1, self._deadline()] + func_params: list[Any] = [qty, 1, self._deadline()] if not recipient: function = token_funcs.tokenToEthSwapInput(*func_params) else: @@ -630,9 +658,7 @@ def _token_to_eth_swap_input( (1 - slippage) * self._get_token_eth_input_price(input_token, qty, fee) ) if fee_on_transfer: - func = ( - self.router.functions.swapExactTokensForETHSupportingFeeOnTransferTokens - ) + func = self.router.functions.swapExactTokensForETHSupportingFeeOnTransferTokens else: func = self.router.functions.swapExactTokensForETH return self._build_and_send_tx( @@ -649,7 +675,7 @@ def _token_to_eth_swap_input( recipient = self.address if fee_on_transfer: - raise Exception("fee on transfer not supported by Uniswap v3") + raise ValueError("fee on transfer not supported by Uniswap v3") output_token = self.get_weth_address() min_tokens_bought = int( @@ -658,8 +684,8 @@ def _token_to_eth_swap_input( ) sqrtPriceLimitX96 = 0 - swap_data = self.router.encodeABI( - fn_name="exactInputSingle", + swap_data = self.router.encode_abi( + "exactInputSingle", args=[ ( input_token, @@ -674,8 +700,8 @@ def _token_to_eth_swap_input( ], ) - unwrap_data = self.router.encodeABI( - fn_name="unwrapWETH9", args=[min_tokens_bought, recipient] + unwrap_data = self.router.encode_abi( + "unwrapWETH9", args=[min_tokens_bought, recipient] ) # Multicall @@ -691,10 +717,12 @@ def _token_to_token_swap_input( input_token: AddressLike, output_token: AddressLike, qty: int, - recipient: Optional[AddressLike], + recipient: AddressLike | None, fee: int, slippage: float, fee_on_transfer: bool = False, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> HexBytes: """Convert tokens to tokens given an input amount.""" # Balance check @@ -705,9 +733,9 @@ def _token_to_token_swap_input( if recipient is None: recipient = self.address - if input_token == ETH_ADDRESS: - raise ValueError - elif output_token == ETH_ADDRESS: + if (input_token == ETH_ADDRESS or output_token == ETH_ADDRESS) and ( + route is None or fees is None + ): raise ValueError if self.version == 1: @@ -730,54 +758,76 @@ def _token_to_token_swap_input( function = token_funcs.tokenToTokenTransferInput(*func_params) return self._build_and_send_tx(function) elif self.version == 2: + if fee_on_transfer: + func = self.router.functions.swapExactTokensForTokensSupportingFeeOnTransferTokens + else: + func = self.router.functions.swapExactTokensForTokens + if route is None: + weth_address = self.get_weth_address() + if is_same_address(input_token, weth_address) or is_same_address( + output_token, weth_address + ): + path = [input_token, output_token] + else: + path = [input_token, weth_address, output_token] + else: + path = route min_tokens_bought = int( (1 - slippage) * self._get_token_token_input_price( - input_token, output_token, qty, fee=fee + input_token, output_token, qty, fee=fee, route=path, fees=fees ) ) - if fee_on_transfer: - func = ( - self.router.functions.swapExactTokensForTokensSupportingFeeOnTransferTokens - ) - else: - func = self.router.functions.swapExactTokensForTokens return self._build_and_send_tx( func( qty, min_tokens_bought, - [input_token, self.get_weth_address(), output_token], + path, recipient, self._deadline(), ), ) elif self.version == 3: if fee_on_transfer: - raise Exception("fee on transfer not supported by Uniswap v3") + raise ValueError("fee on transfer not supported by Uniswap v3") min_tokens_bought = int( (1 - slippage) * self._get_token_token_input_price( - input_token, output_token, qty, fee=fee + input_token, output_token, qty, fee=fee, route=route, fees=fees ) ) sqrtPriceLimitX96 = 0 - - return self._build_and_send_tx( - self.router.functions.exactInputSingle( - { - "tokenIn": input_token, - "tokenOut": output_token, - "fee": fee, - "recipient": recipient, - "deadline": self._deadline(), - "amountIn": qty, - "amountOutMinimum": min_tokens_bought, - "sqrtPriceLimitX96": sqrtPriceLimitX96, - } - ), - self._get_tx_params(), - ) + if route and fees: + encoded_v3_path = self.encode_v3_path(route, fees) + return self._build_and_send_tx( + self.router.functions.exactInput( + { + "path": encoded_v3_path, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + } + ), + self._get_tx_params(), + ) + else: + return self._build_and_send_tx( + self.router.functions.exactInputSingle( + { + "tokenIn": input_token, + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + } + ), + self._get_tx_params(), + ) else: raise ValueError # pragma: no cover @@ -785,7 +835,7 @@ def _eth_to_token_swap_output( self, output_token: AddressLike, qty: int, - recipient: Optional[AddressLike], + recipient: AddressLike | None, fee: int, slippage: float, ) -> HexBytes: @@ -806,7 +856,7 @@ def _eth_to_token_swap_output( token_funcs = self._exchange_contract(output_token).functions eth_qty = self._get_eth_token_output_price(output_token, qty) tx_params = self._get_tx_params(eth_qty) - func_params: List[Any] = [qty, self._deadline()] + func_params: list[Any] = [qty, self._deadline()] if not recipient: function = token_funcs.ethToTokenSwapOutput(*func_params) else: @@ -835,8 +885,8 @@ def _eth_to_token_swap_output( sqrtPriceLimitX96 = 0 - swap_data = self.router.encodeABI( - fn_name="exactOutputSingle", + swap_data = self.router.encode_abi( + "exactOutputSingle", args=[ ( self.get_weth_address(), @@ -851,7 +901,7 @@ def _eth_to_token_swap_output( ], ) - refund_data = self.router.encodeABI(fn_name="refundETH", args=None) + refund_data = self.router.encode_abi("refundETH", args=None) # Multicall return self._build_and_send_tx( @@ -865,7 +915,7 @@ def _token_to_eth_swap_output( self, input_token: AddressLike, qty: Wei, - recipient: Optional[AddressLike], + recipient: AddressLike | None, fee: int, slippage: float, ) -> HexBytes: @@ -896,7 +946,7 @@ def _token_to_eth_swap_output( max_tokens = int((1 + slippage) * inputAmount) ex = self._exchange_contract(input_token) - func_params: List[Any] = [qty, max_tokens, self._deadline()] + func_params: list[Any] = [qty, max_tokens, self._deadline()] if not recipient: function = ex.functions.tokenToEthSwapOutput(*func_params) else: @@ -923,8 +973,8 @@ def _token_to_eth_swap_output( sqrtPriceLimitX96 = 0 - swap_data = self.router.encodeABI( - fn_name="exactOutputSingle", + swap_data = self.router.encode_abi( + "exactOutputSingle", args=[ ( input_token, @@ -939,9 +989,7 @@ def _token_to_eth_swap_output( ], ) - unwrap_data = self.router.encodeABI( - fn_name="unwrapWETH9", args=[qty, recipient] - ) + unwrap_data = self.router.encode_abi("unwrapWETH9", args=[qty, recipient]) # Multicall return self._build_and_send_tx( @@ -956,22 +1004,26 @@ def _token_to_token_swap_output( input_token: AddressLike, output_token: AddressLike, qty: int, - recipient: Optional[AddressLike], + recipient: AddressLike | None, fee: int, slippage: float, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> HexBytes: """Convert tokens to tokens given an output amount. :param fee: TODO """ - if input_token == ETH_ADDRESS: - raise ValueError - elif output_token == ETH_ADDRESS: + if (input_token == ETH_ADDRESS or output_token == ETH_ADDRESS) and ( + route is None or fees is None + ): raise ValueError # Balance check input_balance = self.get_token_balance(input_token) - cost = self._get_token_token_output_price(input_token, output_token, qty, fee) + cost = self._get_token_token_output_price( + input_token, output_token, qty, fee, route, fees + ) amount_in_max = int((1 + slippage) * cost) if ( amount_in_max > input_balance @@ -1000,15 +1052,25 @@ def _token_to_token_swap_output( elif self.version == 2: if recipient is None: recipient = self.address + if route is None: + weth = self.get_weth_address() + path = ( + [input_token, output_token] + if is_same_address(input_token, weth) + or is_same_address(output_token, weth) + else [input_token, weth, output_token] + ) + else: + path = route cost = self._get_token_token_output_price( - input_token, output_token, qty, fee=fee + input_token, output_token, qty, fee=fee, route=path, fees=fees ) amount_in_max = int((1 + slippage) * cost) return self._build_and_send_tx( self.router.functions.swapTokensForExactTokens( qty, amount_in_max, - [input_token, self.get_weth_address(), output_token], + path, recipient, self._deadline(), ), @@ -1019,21 +1081,36 @@ def _token_to_token_swap_output( sqrtPriceLimitX96 = 0 - return self._build_and_send_tx( - self.router.functions.exactOutputSingle( - { - "tokenIn": input_token, - "tokenOut": output_token, - "fee": fee, - "recipient": recipient, - "deadline": self._deadline(), - "amountOut": qty, - "amountInMaximum": amount_in_max, - "sqrtPriceLimitX96": sqrtPriceLimitX96, - }, - ), - self._get_tx_params(), - ) + if route and fees: + encoded_v3_path = self.encode_v3_path(route, fees, is_exact_out=True) + return self._build_and_send_tx( + self.router.functions.exactOutput( + { + "path": encoded_v3_path, + "recipient": recipient, + "deadline": self._deadline(), + "amountOut": qty, + "amountInMaximum": amount_in_max, + } + ), + self._get_tx_params(), + ) + else: + return self._build_and_send_tx( + self.router.functions.exactOutputSingle( + { + "tokenIn": input_token, + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountOut": qty, + "amountInMaximum": amount_in_max, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + }, + ), + self._get_tx_params(), + ) else: raise ValueError @@ -1077,7 +1154,6 @@ def get_exchange_rate(self, token: AddressLike) -> float: # ------ Liquidity ----------------------------------------------------------------- @supports([1]) - @check_approval def add_liquidity( self, token: AddressLike, max_eth: Wei, min_liquidity: int = 1 ) -> HexBytes: @@ -1091,13 +1167,12 @@ def add_liquidity( return self._build_and_send_tx(function, tx_params) @supports([1]) - @check_approval def remove_liquidity(self, token: str, max_token: int) -> HexBytes: """Remove liquidity from the pool.""" func_params = [int(max_token), 1, 1, self._deadline()] - function = self._exchange_contract(token).functions.removeLiquidity( - *func_params - ) + function = self._exchange_contract( + self.w3.to_checksum_address(token) + ).functions.removeLiquidity(*func_params) return self._build_and_send_tx(function) @supports([3]) @@ -1172,7 +1247,7 @@ def close_position( tokenId: int, amount0Min: int = 0, amount1Min: int = 0, - deadline: Optional[int] = None, + deadline: int | None = None, ) -> TxReceipt: """ remove all liquidity from the position associated w/ tokenId, collect fees, and burn token. @@ -1252,12 +1327,12 @@ def get_min_tick_from_wordpos( # Find min or max tick in initialized tick range using the tickBitmap def find_tick_from_bitmap( self, - bitmap_spacing: Tuple[int, int], + bitmap_spacing: tuple[int, int], pool: Contract, tick_spacing: int, fee: int, left: bool = True, - ) -> Union[int, bool]: + ) -> int | bool: # searching to the left (finding max tick) if left: min_wordPos = bitmap_spacing[1] @@ -1295,7 +1370,7 @@ def find_tick_from_bitmap( return _min_tick return False - def get_tvl_in_pool(self, pool: Contract) -> Tuple[float, float]: + def get_tvl_in_pool(self, pool: Contract) -> tuple[float, float]: """ Iterate through each tick in a pool and calculate the TVL on-chain @@ -1388,7 +1463,7 @@ def get_tvl_in_pool(self, pool: Contract) -> Tuple[float, float]: return (token0_liquidity, token1_liquidity) # ------ Approval Utils ------------------------------------------------------------ - def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: + def approve(self, token: AddressLike, max_approval: int | None = None) -> TxReceipt: """Give an exchange/router max approval of a token.""" max_approval = self.max_approval_int if not max_approval else max_approval contract_addr = ( @@ -1401,10 +1476,11 @@ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> Non ) logger.warning(f"Approving {_addr_to_str(token)}...") tx = self._build_and_send_tx(function) - self.w3.eth.wait_for_transaction_receipt(tx, timeout=6000) + receipt = self.w3.eth.wait_for_transaction_receipt(tx, timeout=6000) # Add extra sleep to let tx propagate correctly time.sleep(1) + return receipt def _is_approved(self, token: AddressLike) -> bool: """Check to see if the exchange and token is approved.""" @@ -1420,10 +1496,7 @@ def _is_approved(self, token: AddressLike) -> bool: .functions.allowance(self.address, contract_addr) .call() ) - if amount >= self.max_approval_check_int: - return True - else: - return False + return bool(amount >= self.max_approval_check_int) # ------ Tx Utils ------------------------------------------------------------------ def _deadline(self) -> int: @@ -1431,24 +1504,27 @@ def _deadline(self) -> int: return int(time.time()) + 10 * 60 def _build_and_send_tx( - self, function: ContractFunction, tx_params: Optional[TxParams] = None + self, function: ContractFunction, tx_params: TxParams | None = None ) -> HexBytes: """Build and send a transaction.""" if not tx_params: tx_params = self._get_tx_params() + + # Pre-populate gas BEFORE build_transaction to prevent web3 from calling + # eth_estimateGas internally. web3 calls eth_estimateGas during + # build_transaction when no gas is provided, which fails for contracts + # with computed jumps (e.g. Vyper v1 exchange contracts) under Anvil's + # strict EVM. use_estimate_gas=True for networks like Arbitrum where 500k + # is not a safe default. + if "gas" not in tx_params and not self.use_estimate_gas: + tx_params["gas"] = Wei(500_000) + transaction = function.build_transaction(tx_params) if "gas" not in tx_params: - # `use_estimate_gas` needs to be True for networks like Arbitrum (can't assume 250000 gas), - # but it breaks tests for unknown reasons because estimate_gas takes forever on some tx's. - # Maybe an issue with ganache? (got GC warnings once...) - if self.use_estimate_gas: - # The Uniswap V3 UI uses 20% margin for transactions - transaction["gas"] = Wei( - int(self.w3.eth.estimate_gas(transaction) * 1.2) - ) - else: - transaction["gas"] = Wei(250_000) + # use_estimate_gas=True: run explicit estimate with 20% margin + # The Uniswap V3 UI uses 20% margin for transactions + transaction["gas"] = Wei(int(self.w3.eth.estimate_gas(transaction) * 1.2)) signed_txn = self.w3.eth.account.sign_transaction( transaction, private_key=self.private_key @@ -1456,14 +1532,12 @@ def _build_and_send_tx( # TODO: This needs to get more complicated if we want to support replacing a transaction # FIXME: This does not play nice if transactions are sent from other places using the same wallet. try: - return self.w3.eth.send_raw_transaction(signed_txn.rawTransaction) + return self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) finally: logger.debug(f"nonce: {tx_params['nonce']}") self.last_nonce = Nonce(tx_params["nonce"] + 1) - def _get_tx_params( - self, value: Wei = Wei(0), gas: Optional[Wei] = None - ) -> TxParams: + def _get_tx_params(self, value: Wei = Wei(0), gas: Wei | None = None) -> TxParams: """Get generic transaction parameters.""" params: TxParams = { "from": _addr_to_str(self.address), @@ -1481,7 +1555,7 @@ def _get_tx_params( # ------ Price Calculation Utils --------------------------------------------------- def _calculate_max_input_token( self, input_token: AddressLike, qty: int, output_token: AddressLike - ) -> Tuple[int, int]: + ) -> tuple[int, int]: """ For buy orders (exact output), the cost (input) is calculated. Calculate the max input and max eth sold for a token to token output swap. @@ -1513,7 +1587,7 @@ def _calculate_max_input_token( def _calculate_max_output_token( self, output_token: AddressLike, qty: int, input_token: AddressLike - ) -> Tuple[int, int]: + ) -> tuple[int, int]: """ For sell orders (exact input), the amount bought (output) is calculated. Similar to _calculate_max_input_token, but for an exact input swap. @@ -1545,9 +1619,9 @@ def _calculate_max_output_token( # Batch contract function calls to speed up large on-chain data queries def multicall( self, - encoded_functions: Sequence[Tuple[ChecksumAddress, bytes]], + encoded_functions: Sequence[tuple[ChecksumAddress, bytes]], output_types: Sequence[str], - ) -> List[Any]: + ) -> list[Any]: """ Calls aggregate() on Uniswap Multicall2 contract @@ -1598,28 +1672,29 @@ def get_token(self, address: AddressLike, abi_name: str = "erc20") -> ERC20Token _name = token_contract.functions.name().call() _symbol = token_contract.functions.symbol().call() decimals = token_contract.functions.decimals().call() - except Exception as e: + except ValueError as e: logger.warning( f"Exception occurred while trying to get token {_addr_to_str(address)}: {e}" ) raise InvalidToken(address) try: name = _name.decode() - except Exception: + except (ValueError, AttributeError): name = _name try: symbol = _symbol.decode() - except Exception: + except (ValueError, AttributeError): symbol = _symbol return ERC20Token(symbol, address, name, decimals) - @functools.lru_cache() - @supports([2, 3]) + @supports([1, 2, 3]) def get_weth_address(self) -> ChecksumAddress: """Retrieves the WETH address from the contracts (which may vary between chains).""" - if self.version == 2: + if self.version == 1: + address: ChecksumAddress = self.w3.to_checksum_address(WETH9_ADDRESS) + elif self.version == 2: # Contract calls should always return checksummed addresses - address: ChecksumAddress = self.router.functions.WETH().call() + address = self.router.functions.WETH().call() elif self.version == 3: address = self.router.functions.WETH9().call() else: @@ -1679,7 +1754,7 @@ def create_pool_instance( return pool_instance @supports([3]) - def get_pool_immutables(self, pool: Contract) -> Dict: + def get_pool_immutables(self, pool: Contract) -> dict: """ Fetch on-chain pool data. """ @@ -1695,7 +1770,7 @@ def get_pool_immutables(self, pool: Contract) -> Dict: return pool_immutables @supports([3]) - def get_pool_state(self, pool: Contract) -> Dict: + def get_pool_state(self, pool: Contract) -> dict: """ Fetch on-chain pool state. """ @@ -1715,12 +1790,12 @@ def get_pool_state(self, pool: Contract) -> Dict: return pool_state @supports([3]) - def get_liquidity_positions(self) -> List[int]: + def get_liquidity_positions(self) -> list[int]: """ Enumerates liquidity position tokens owned by address. Returns array of token IDs. """ - positions: List[int] = [] + positions: list[int] = [] number_of_positions = self.nonFungiblePositionManager.functions.balanceOf( _addr_to_str(self.address) ).call() @@ -1763,8 +1838,8 @@ def mint_position(self, pool: Contract, amount0: int, amount1: int) -> HexBytes: # tx_mint = pool.functions.mint(self.address, MIN_TICK, MAX_TICK, amount0,'').transact(); - position = positionManager.encodeABI( - fn_name="mint", + position = positionManager.encode_abi( + "mint", args=[ { "token0": token0, @@ -1803,9 +1878,49 @@ def mint_position(self, pool: Contract, amount0: int, amount1: int) -> HexBytes: return multicall + @supports([3]) + def encode_v3_path( + self, route: list[AddressLike], fees: list[int], is_exact_out: bool = False + ) -> bytes: + """ + Encodes a Uniswap V3 path for use in swap and quote functions. + + :param route : A list of token addresses representing the swap route. The length of this list must be one greater than the length of the `fees` list. + :param fees : A list of fee tiers corresponding to each hop in the route. Each fee tier should be an integer representing the fee in hundredths of a bip (e.g., 3000 for 0.3%). + :param is_exact_out : A boolean indicating whether the swap is an exact output swap. If True, the path will be encoded in reverse order. + + :return : A bytes object representing the encoded path. + """ + if len(route) < 2: + raise ValueError( + "Route must contain at least two token addresses. " + f"Got route length {len(route)}." + ) + if len(route) != len(fees) + 1: + raise ValueError( + "Route length must be one greater than fees length. " + f"Got route length {len(route)} and fees length {len(fees)}." + ) + + path = b"" + _route = route.copy() + _fees = fees.copy() + if is_exact_out: + # For exact output swaps, the path is encoded in reverse order + _route.reverse() + _fees.reverse() + + for i in range(len(_fees)): + path += self.w3.to_bytes(hexstr=HexStr(_addr_to_str(_route[i]))) + _fees[ + i + ].to_bytes(3, "big") + path += self.w3.to_bytes(hexstr=HexStr(_addr_to_str(_route[-1]))) + + return path + @supports([2, 3]) def get_raw_price( - self, token_in: AddressLike, token_out: AddressLike, fee: Optional[int] = None + self, token_in: AddressLike, token_out: AddressLike, fee: int | None = None ) -> float: """ Returns current price for pair of tokens [token_in, token_out] regrading liquidity that is being locked in the pool @@ -1820,7 +1935,7 @@ def get_raw_price( token_out = self.get_weth_address() if self.version == 2: - params: Iterable[Union[ChecksumAddress, Optional[int]]] = [ + params: Iterable[ChecksumAddress | int | None] = [ self.w3.to_checksum_address(token_in), self.w3.to_checksum_address(token_out), ] @@ -1880,7 +1995,7 @@ def estimate_price_impact( token_out: AddressLike, amount_in: int, fee: int, - route: Optional[List[AddressLike]] = None, + route: list[AddressLike] | None = None, ) -> float: """ Returns the estimated price impact as a positive float (0.01 = 1%). @@ -1955,12 +2070,11 @@ def _token_address_from_exchange(self, exchange_addr: AddressLike) -> Address: ) return token_addr - @functools.lru_cache() @supports([1]) def _exchange_contract( self, - token_addr: Optional[AddressLike] = None, - ex_addr: Optional[AddressLike] = None, + token_addr: AddressLike | None = None, + ex_addr: AddressLike | None = None, ) -> Contract: if not ex_addr and token_addr: ex_addr = self._exchange_address_from_token(token_addr) @@ -1972,7 +2086,7 @@ def _exchange_contract( return contract @supports([1]) - def _get_all_tokens(self) -> List[ERC20Token]: + def _get_all_tokens(self) -> list[ERC20Token]: """ Retrieves all token pairs. diff --git a/uniswap/uniswap4.py b/uniswap/uniswap4.py index bd9d7829..78fc413f 100644 --- a/uniswap/uniswap4.py +++ b/uniswap/uniswap4.py @@ -1,17 +1,8 @@ -# ruff: noqa: UP035 -# ruff: noqa: UP006 -# ruff: noqa: UP007 -# ruff: noqa: UP045 -# ruff: noqa: TRY002 -# ruff: noqa: B006 -# ruff: noqa: BLE001 -# ruff: noqa: SIM102 import logging import os import time from dataclasses import astuple from decimal import Decimal -from typing import Dict, List, Optional, Tuple, Union import eth_abi.abi from eth_abi import encode @@ -79,10 +70,10 @@ class Uniswap4: def __init__( self, - address: Union[str, AddressLike], - private_key: Optional[str] = None, - provider: Optional[str] = None, - web3: Optional[Web3] = None, + address: str | AddressLike, + private_key: str | None = None, + provider: str | None = None, + web3: Web3 | None = None, max_slippage: float = 0.01, gas_limit: float = 250000.0, gas_price: float = 1.80, @@ -138,9 +129,9 @@ def __init__( if self.net_id in _netid_to_name: self.net_name = _netid_to_name[self.net_id] if self.net_name not in _quoter_contract_addresses_v4: - raise Exception(f"Netid {self.net_id} is not supported yet.") + raise ValueError(f"Netid {self.net_id} is not supported yet.") else: - raise Exception(f"Unknown netid: {self.net_id}") + raise ValueError(f"Unknown netid: {self.net_id}") logger.info(f"Using {self.w3} ('{self.net_name}', netid: {self.net_id})") quoter_address = _quoter_contract_addresses_v4[self.net_name] @@ -239,8 +230,8 @@ def __init__( def approve( self, token: AddressLike, - max_approval: Optional[int] = None, - delay_interval: Optional[int] = 7, + max_approval: int | None = None, + delay_interval: int | None = 7, approve_position_manager: bool = False, ) -> HexBytes: """Approve the router to spend a token on the user's behalf, or set up a permit for the position manager to pull the token from the user's wallet. For ETH, the router can pull from the user's wallet directly, so no approval is necessary. @@ -357,7 +348,7 @@ def stateview_get_fee_growth_globals( fee: int, tick_spacing: int, hooks: str, - ) -> Dict: + ) -> dict: """ Retrieves the global fee growth of a pool. """ @@ -366,7 +357,7 @@ def stateview_get_fee_growth_globals( pool = PoolKey(token0, token1, fee, tick_spacing, hooks) pool_id = self.get_pool_id(pool) - fee_growth_globals: Dict = self.stateview.functions.getFeeGrowthGlobals( + fee_growth_globals: dict = self.stateview.functions.getFeeGrowthGlobals( pool_id ).call() return_value = { @@ -384,7 +375,7 @@ def stateview_get_fee_growth_inside( hooks: str, tick_lower: int, tick_upper: int, - ) -> Dict: + ) -> dict: """ Calculates the fee growth inside a tick range of a pool """ @@ -393,7 +384,7 @@ def stateview_get_fee_growth_inside( pool = PoolKey(token0, token1, fee, tick_spacing, hooks) pool_id = self.get_pool_id(pool) - fee_growth_inside: Dict = self.stateview.functions.getFeeGrowthInside( + fee_growth_inside: dict = self.stateview.functions.getFeeGrowthInside( pool_id, tick_lower, tick_upper ).call() return_value = { @@ -431,7 +422,7 @@ def stateview_get_position_info( tick_lower: int, tick_upper: int, token_id: int, - ) -> Dict: + ) -> dict: """ Retrieves position info in a pool. @@ -444,7 +435,7 @@ def stateview_get_position_info( pool_id = self.get_pool_id(pool) salt = HexBytes(token_id.to_bytes(32, byteorder="big")) - position_info: Dict = self.stateview.functions.getPositionInfo( + position_info: dict = self.stateview.functions.getPositionInfo( pool_id, owner, tick_lower, tick_upper, salt ).call() return_value = { @@ -461,7 +452,7 @@ def stateview_get_slot0( fee: int, tick_spacing: int, hooks: str, - ) -> Dict: + ) -> dict: """ Returns current state of the pool. """ @@ -471,7 +462,7 @@ def stateview_get_slot0( pool = PoolKey(token0, token1, fee, tick_spacing, hooks) pool_id = self.get_pool_id(pool) - slot: Dict = self.stateview.functions.getSlot0(pool_id).call() + slot: dict = self.stateview.functions.getSlot0(pool_id).call() return_value = { "sqrtPriceX96": slot[0], "tick": slot[1], @@ -511,7 +502,7 @@ def stateview_get_tick_fee_growth_outside( tick_spacing: int, hooks: str, tick: int, - ) -> Dict: + ) -> dict: """ Retrieves the fee growth outside a tick range of a pool """ @@ -520,7 +511,7 @@ def stateview_get_tick_fee_growth_outside( pool = PoolKey(token0, token1, fee, tick_spacing, hooks) pool_id = self.get_pool_id(pool) - fee_growth_outside: Dict = self.stateview.functions.getTickFeeGrowthOutside( + fee_growth_outside: dict = self.stateview.functions.getTickFeeGrowthOutside( pool_id, tick ).call() return_value = { @@ -537,7 +528,7 @@ def stateview_get_tick_pool_info( tick_spacing: int, hooks: str, tick: int, - ) -> Dict: + ) -> dict: """ Retrieves the tick information of a pool at a specific tick. """ @@ -546,7 +537,7 @@ def stateview_get_tick_pool_info( pool = PoolKey(token0, token1, fee, tick_spacing, hooks) pool_id = self.get_pool_id(pool) - tick_info: Dict = self.stateview.functions.getTickInfo(pool_id, tick).call() + tick_info: dict = self.stateview.functions.getTickInfo(pool_id, tick).call() return_value = { "liquidityGross": tick_info[0], "liquidityNet": tick_info[1], @@ -558,7 +549,7 @@ def stateview_get_tick_pool_info( # ReservesLens methods def reserves_lens_get_pool_tvl( self, pool_key: PoolKey, custom_provider: str = "" - ) -> Dict: + ) -> dict: """ Retrieves the total value locked (TVL) of a pool. See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. @@ -568,7 +559,7 @@ def reserves_lens_get_pool_tvl( :returns: A dictionary containing the reserves of the pool. """ if custom_provider == "": - reserves: Dict = self.reserves_lens.functions.getPoolTVL( + reserves: dict = self.reserves_lens.functions.getPoolTVL( _addr_to_str(self.pool_manager_address), astuple(pool_key) ).call() else: @@ -596,8 +587,8 @@ def reserves_lens_get_pool_tvl( return return_value def reserves_lens_get_pool_tvl_batch( - self, pool_keys: List[PoolKey], custom_provider: Optional[List[str]] = None - ) -> List[Dict]: + self, pool_keys: list[PoolKey], custom_provider: list[str] | None = None + ) -> list[dict]: """ Retrieves the total value locked (TVL) of multiple pools in a batch. See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. @@ -609,7 +600,7 @@ def reserves_lens_get_pool_tvl_batch( if custom_provider is None: custom_provider = [""] * len(pool_keys) - reserves_list: List = self.reserves_lens.functions.getPoolTVLBatch( + reserves_list: list = self.reserves_lens.functions.getPoolTVLBatch( _addr_to_str(self.pool_manager_address), [astuple(pool_key) for pool_key in pool_keys], ).call() @@ -649,7 +640,7 @@ def reserves_lens_get_tvl_paged( cursor: bytes, custom_provider: str = "", max_reads: int = 0, - ) -> Tuple[Dict, bytes, bool]: + ) -> tuple[dict, bytes, bool]: """ Retrieves the total value locked (TVL) of multiple pools in a paged manner. See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. @@ -679,7 +670,7 @@ def reserves_lens_get_tvl_paged( ).call() ) - reserves: Dict = { + reserves: dict = { "coreAmount0": reserves_tuple[0], "coreAmount1": reserves_tuple[1], "hookReserves0": reserves_tuple[2], @@ -701,7 +692,7 @@ def reserves_lens_get_tvl_paged( def reserves_lens_get_populated_ticks_in_word( self, pool_key: PoolKey, word_position: int - ) -> List[Dict]: + ) -> list[dict]: """ Retrieves the populated ticks in a specific word of a pool. See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. @@ -710,12 +701,12 @@ def reserves_lens_get_populated_ticks_in_word( :param word_position: The position of the word to retrieve. :returns: A list of dictionaries containing the populated ticks. """ - results: List = self.reserves_lens.functions.getPopulatedTicksInWord( + results: list = self.reserves_lens.functions.getPopulatedTicksInWord( _addr_to_str(self.pool_manager_address), astuple(pool_key), word_position, ).call() - return_value: List[Dict] = [] + return_value: list[dict] = [] for result in results: return_value.append( { @@ -835,7 +826,7 @@ def position_manager_get_approved(self, token_id: int) -> str: return_value = operator return return_value - def position_manager_get_pool_and_position_info(self, token_id: int) -> Dict: + def position_manager_get_pool_and_position_info(self, token_id: int) -> dict: """ :returns: The PoolKey class object and position info of a position """ @@ -1058,7 +1049,7 @@ def position_manager_modify_liquidities( return tx def position_manager_modify_liquidities_without_unlock( - self, actions: bytes, params: List[bytes], payable_amount: int + self, actions: bytes, params: list[bytes], payable_amount: int ) -> HexBytes: """ Batches actions for modifying liquidity without unlocking v4 PoolManager @@ -1074,7 +1065,7 @@ def position_manager_modify_liquidities_without_unlock( return tx def position_manager_multicall( - self, data: List[bytes], payable_amount: int + self, data: list[bytes], payable_amount: int ) -> HexBytes: """ Call multiple functions in the current contract in a single transaction, with the possibility of sending ETH along with the calls. @@ -1293,29 +1284,29 @@ def pool_manager_get_extsload(self, slot: bytes) -> bytes: def pool_manager_get_extsload_sequence( self, start_slot: bytes, slots_count: int - ) -> List[bytes]: + ) -> list[bytes]: """ Called by external contracts to access a sequence of storage slots """ - value: List[bytes] = self.pool_manager.functions.extsload( + value: list[bytes] = self.pool_manager.functions.extsload( start_slot, slots_count ).call() return_value = value return return_value - def pool_manager_get_extsload_sparse(self, slots: List[bytes]) -> List[bytes]: + def pool_manager_get_extsload_sparse(self, slots: list[bytes]) -> list[bytes]: """ Called by external contracts to access a sparse set of storage slots """ - value: List[bytes] = self.pool_manager.functions.extsload(slots).call() + value: list[bytes] = self.pool_manager.functions.extsload(slots).call() return_value = value return return_value - def pool_manager_get_exttload_sparse(self, slots: List[bytes]) -> List[bytes]: + def pool_manager_get_exttload_sparse(self, slots: list[bytes]) -> list[bytes]: """ Called by external contracts to access sparse transient pool state """ - value: List[bytes] = self.pool_manager.functions.exttload(slots).call() + value: list[bytes] = self.pool_manager.functions.exttload(slots).call() return_value = value return return_value @@ -1739,7 +1730,7 @@ def get_quote_exact_input( self, token_exact: str, qty: int, - route: List[PoolKey], + route: list[PoolKey], ) -> int: """ :param token_exact: The token for which the qty parameter is specified. @@ -1802,7 +1793,7 @@ def get_quote_exact_output( self, token_exact: str, qty: int, - route: List[PoolKey], + route: list[PoolKey], ) -> int: """ :param token_exact: The token for which the qty parameter is specified, either the input or output token depending on the quote type. @@ -1827,11 +1818,11 @@ def get_price_input( token0: str, token1: str, qty: int, - fee: Optional[int] = None, - tick_spacing: Optional[int] = None, - hooks: Optional[str] = ZERO_HOOK, - hook_data: Optional[bytes] = b"", - route: Optional[List[PoolKey]] = None, + fee: int | None = None, + tick_spacing: int | None = None, + hooks: str | None = ZERO_HOOK, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, ) -> int: """ :param token0: The token to be sold. @@ -1871,11 +1862,11 @@ def get_price_output( token0: str, token1: str, qty: int, - fee: Optional[int] = None, - tick_spacing: Optional[int] = None, - hooks: Optional[str] = ZERO_HOOK, - hook_data: Optional[bytes] = b"", - route: Optional[List[PoolKey]] = None, + fee: int | None = None, + tick_spacing: int | None = None, + hooks: str | None = ZERO_HOOK, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, ) -> int: """ :param token0: The token to be sold. @@ -1921,7 +1912,7 @@ def token_to_token_swap_exact_input( hooks: str, hook_data: bytes = b"", min_hop_price_x_36: int = 0, - custom_nonce: Optional[Nonce] = None, + custom_nonce: Nonce | None = None, ) -> HexBytes: """ :param input_token: The token to be sold. @@ -2010,9 +2001,9 @@ def token_to_token_swap_input( input_token: str, qty: int, qtycap: int, - route: List[PoolKey], - min_hop_price_x_36: List[int] = [], - custom_nonce: Optional[Nonce] = None, + route: list[PoolKey], + min_hop_price_x_36: list[int] = [], + custom_nonce: Nonce | None = None, ) -> HexBytes: """ :param input_token: The token to be sold. @@ -2102,7 +2093,7 @@ def token_to_token_swap_exact_output( hooks: str, hook_data: bytes = b"", min_hop_price_x_36: int = 0, - custom_nonce: Optional[Nonce] = None, + custom_nonce: Nonce | None = None, ) -> HexBytes: """ :param input_token: The token to be sold. @@ -2203,9 +2194,9 @@ def token_to_token_swap_output( output_token: str, qty: int, qtycap: int, - route: List[PoolKey], - min_hop_price_x_36: List[int] = [], - custom_nonce: Optional[Nonce] = None, + route: list[PoolKey], + min_hop_price_x_36: list[int] = [], + custom_nonce: Nonce | None = None, ) -> HexBytes: """ :param output_token: The token to be bought. @@ -2295,7 +2286,7 @@ def drop_txn( address_to: AddressLike, gas_price: float, priority_fee: int = 10, - custom_nonce: Optional[Nonce] = None, + custom_nonce: Nonce | None = None, ) -> HexBytes: """ Replaces pending transaction with zero-value ETH transfer @@ -2340,9 +2331,9 @@ def drop_txn( self.private_key, ) if self.post_merge: - return self.w3.eth.send_raw_transaction(signed_txn_london.rawTransaction) + return self.w3.eth.send_raw_transaction(signed_txn_london.raw_transaction) else: - return self.w3.eth.send_raw_transaction(signed_txn.rawTransaction) + return self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) # Market functions for swapping `qty` amount of `token0` to buy `token1` def make_swap_input( @@ -2351,10 +2342,10 @@ def make_swap_input( output_token: str, qty: int, qtycap: int, - swap_pool_key: Optional[PoolKey] = None, - hook_data: Optional[bytes] = b"", - route: Optional[List[PoolKey]] = None, - custom_nonce: Optional[Nonce] = None, + swap_pool_key: PoolKey | None = None, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + custom_nonce: Nonce | None = None, ) -> HexBytes: """ :param input_token: The token to be sold. @@ -2368,7 +2359,7 @@ def make_swap_input( Make a trade by defining the qty of the input token. If `route` is provided, it will be used for the swap. Otherwise, `swap_pool_key` must be provided for a single hop swap.""" - result: Optional[HexBytes] = None + result: HexBytes | None = None if route is None: if swap_pool_key is None: raise ValueError("swap_pool_key must be provided for single hop swaps") @@ -2401,10 +2392,10 @@ def make_swap_output( output_token: str, qty: int, qtycap: int, - swap_pool_key: Optional[PoolKey] = None, - hook_data: Optional[bytes] = b"", - route: Optional[List[PoolKey]] = None, - custom_nonce: Optional[Nonce] = None, + swap_pool_key: PoolKey | None = None, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + custom_nonce: Nonce | None = None, ) -> HexBytes: """ :param input_token: The token to be sold. @@ -2418,7 +2409,7 @@ def make_swap_output( Make a trade by defining the qty of the output token. If `route` is provided, it will be used for the swap. Otherwise, `swap_pool_key` must be provided for a single hop swap. """ - result: Optional[HexBytes] = None + result: HexBytes | None = None if route is None: if swap_pool_key is None: raise ValueError("swap_pool_key must be provided for single hop swaps") @@ -2446,7 +2437,7 @@ def make_swap_output( return result # Liquidity management functions - def get_position_info(self, token_id: int) -> Dict: + def get_position_info(self, token_id: int) -> dict: """ Get information about a liquidity position given its token ID. :return: A dictionary with the following keys: @@ -2468,7 +2459,7 @@ def get_position_info(self, token_id: int) -> Dict: pool_info = position_info["info"] pool_info_decoded = self.decode_position_info(pool_info) owner_of = self.position_manager_get_owner_of(token_id) - return_value: Dict = { + return_value: dict = { "currency0": pool_key.currency0, "currency1": pool_key.currency1, "fee": pool_key.fee, @@ -2484,7 +2475,7 @@ def get_position_info(self, token_id: int) -> Dict: def get_position_value( self, token_id: int, token0_decimals: int, token1_decimals: int - ) -> Dict: + ) -> dict: """ Get the value of a liquidity position given its token ID. """ @@ -2517,7 +2508,7 @@ def get_position_value( ) amount0: Decimal = Decimal(amounts["amount0"]) / Decimal(10**token0_decimals) amount1: Decimal = Decimal(amounts["amount1"]) / Decimal(10**token1_decimals) - return_value: Dict = { + return_value: dict = { "amount0": amounts["amount0"], "amount1": amounts["amount1"], # NOTE: unclaimed fees are not yet computed; total values equal principal only @@ -2534,7 +2525,7 @@ def create_pool( self, pool_key: PoolKey, sqrt_price_x96: int, - custom_nonce: Optional[Nonce] = None, + custom_nonce: Nonce | None = None, ) -> HexBytes: """ Creates a new liquidity pool without initial liquidity with the specified parameters and a starting price. @@ -2558,9 +2549,9 @@ def mint_position( liquidity: int, amount0: int, amount1: int, - recipient: Optional[str] = None, - hook_data: Optional[bytes] = b"", - custom_nonce: Optional[Nonce] = None, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, ) -> HexBytes: """ Mints a new liquidity position with the specified parameters. @@ -2627,7 +2618,7 @@ def mint_position( v4_actions_abis["SWEEP"], [pool_key.currency1, recipient], ) - params: List[bytes] = [ + params: list[bytes] = [ mint_position_params, settle_pair_params, sweep0_params, @@ -2656,9 +2647,9 @@ def increase_liquidity( amount0_max: int, amount1_max: int, liquidity: int, - recipient: Optional[str] = None, - hook_data: Optional[bytes] = b"", - custom_nonce: Optional[Nonce] = None, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, ) -> HexBytes: """ Increases the liquidity of an existing position. @@ -2712,7 +2703,7 @@ def increase_liquidity( v4_actions_abis["SWEEP"], [pool_key.currency1, recipient], ) - params: List[bytes] = [ + params: list[bytes] = [ increase_liquidity_params, settle_pair_params, sweep0_params, @@ -2741,9 +2732,9 @@ def decrease_liquidity( amount0_min: int, amount1_min: int, liquidity: int, - recipient: Optional[str] = None, - hook_data: Optional[bytes] = b"", - custom_nonce: Optional[Nonce] = None, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, ) -> HexBytes: """ Decreases the liquidity of an existing position. @@ -2783,7 +2774,7 @@ def decrease_liquidity( ["address", "address", "address"], [pool_key.currency0, pool_key.currency1, recipient], ) - params: List[bytes] = [decrease_liquidity_params, take_pair_params] + params: list[bytes] = [decrease_liquidity_params, take_pair_params] # Encoding unlock data unlock_data: bytes = encode( @@ -2804,9 +2795,9 @@ def collect_fees( self, pool_key: PoolKey, token_id: int, - recipient: Optional[str] = None, - hook_data: Optional[bytes] = b"", - custom_nonce: Optional[Nonce] = None, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, ) -> HexBytes: """ Collects the fees accrued by an existing position. @@ -2843,7 +2834,7 @@ def collect_fees( ["address", "address", "address"], [pool_key.currency0, pool_key.currency1, recipient], ) - params: List[bytes] = [decrease_liquidity_params, take_pair_params] + params: list[bytes] = [decrease_liquidity_params, take_pair_params] # Encoding unlock data unlock_data: bytes = encode( @@ -2866,9 +2857,9 @@ def burn_position( token_id: int, amount0_min: int, amount1_min: int, - recipient: Optional[str] = None, - hook_data: Optional[bytes] = b"", - custom_nonce: Optional[Nonce] = None, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, ) -> HexBytes: """ Burns an existing liquidity position. @@ -2906,7 +2897,7 @@ def burn_position( ["address", "address", "address"], [pool_key.currency0, pool_key.currency1, recipient], ) - params: List[bytes] = [burn_position_params, take_pair_params] + params: list[bytes] = [burn_position_params, take_pair_params] # Encoding unlock data unlock_data: bytes = encode( @@ -2925,11 +2916,11 @@ def burn_position( def universal_router_execute( self, - commands: List[int], - actions: List[List[int]], - params: List[List[List]], + commands: list[int], + actions: list[list[int]], + params: list[list[list]], ether_amount: int = 0, - custom_nonce: Optional[Nonce] = None, + custom_nonce: Nonce | None = None, ) -> HexBytes: """ Executes a transaction with the Universal Router with the specified commands, actions, and parameters. @@ -2981,7 +2972,7 @@ def universal_router_execute( # Encoding data commands_abi = ["uint8"] * len(commands) encoded_commands: bytes = encode_packed(commands_abi, commands) - encoded_inputs: List[bytes] = [] + encoded_inputs: list[bytes] = [] for commands_item, actions_item, params_item in zip(commands, actions, params): command_key = self._get_dict_key_by_value( universal_router_commands, commands_item @@ -3016,25 +3007,25 @@ def universal_router_execute( # Helper functions def encode_actions_with_params( - self, actions: List[int], params: List[List] - ) -> Dict: + self, actions: list[int], params: list[list] + ) -> dict: encoded_actions_abi = ["uint8"] * len(actions) encoded_actions: bytes = encode_packed(encoded_actions_abi, actions) - encoded_params: List[bytes] = [] + encoded_params: list[bytes] = [] for actions_item, params_item in zip(actions, params): action_key = self._get_dict_key_by_value(v4_actions, actions_item) encoded_params_item: bytes = encode( v4_actions_abis[action_key], params_item ) encoded_params.append(encoded_params_item) - return_value: Dict = { + return_value: dict = { "actions": encoded_actions, "params": encoded_params, } return return_value @staticmethod - def _get_dict_key_by_value(param_dict: Dict, value: int) -> str: + def _get_dict_key_by_value(param_dict: dict, value: int) -> str: return_value = next((k for k, v in param_dict.items() if v == value), None) if return_value is None: raise IndexError("Key is not found.") @@ -3136,7 +3127,7 @@ def get_amounts_for_liquidity( sqrt_ratio_b_x96: int, sqrt_ratio_current_x96: int, liquidity: int, - ) -> Dict: + ) -> dict: """ Helper function to calculate the amounts of `token0` and `token1` that can be provided for a given amount of liquidity and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. """ @@ -3160,7 +3151,7 @@ def get_amounts_for_liquidity( amount1 = self.get_amount1_for_liquidity( sqrt_ratio_a_x96, sqrt_ratio_b_x96, liquidity ) - return_value: Dict = { + return_value: dict = { "amount0": amount0, "amount1": amount1, } @@ -3168,10 +3159,10 @@ def get_amounts_for_liquidity( def get_amounts_for_liquidity_by_ticks( self, ratio_current_x96: int, tick_lower: int, tick_upper: int, liquidity: int - ) -> Dict: + ) -> dict: sqrt_ratio_a_x96 = get_sqrt_ratio_at_tick(tick_lower) sqrt_ratio_b_x96 = get_sqrt_ratio_at_tick(tick_upper) - return_value: Dict = self.get_amounts_for_liquidity( + return_value: dict = self.get_amounts_for_liquidity( sqrt_ratio_a_x96, sqrt_ratio_b_x96, ratio_current_x96, liquidity ) return return_value @@ -3191,7 +3182,7 @@ def get_liquidity_for_amounts_by_ticks( ) return liquidity - def get_minted_token_id(self, tx_hash: str) -> List[int]: + def get_minted_token_id(self, tx_hash: str) -> list[int]: """ Helper function to extract the token ID of a newly minted position from the transaction receipt of the minting transaction. @@ -3201,7 +3192,7 @@ def get_minted_token_id(self, tx_hash: str) -> List[int]: logs = self.position_manager.events.Transfer().process_receipt( transaction_receipt ) - return_value: List[int] = [] + return_value: list[int] = [] for log in logs: try: if _addr_to_str(log.args["from"]) == ZERO_HOOK: @@ -3215,7 +3206,7 @@ def get_minted_token_id(self, tx_hash: str) -> List[int]: return return_value @staticmethod - def decode_position_info(position_info: int) -> Dict: + def decode_position_info(position_info: int) -> dict: """ :return: @@ -3248,7 +3239,7 @@ def decode_position_info(position_info: int) -> Dict: rest_part: int = position_info >> pool_id_offset pool_id: bytes = rest_part.to_bytes(25, byteorder="big") - return_value: Dict = { + return_value: dict = { "tickLower": tick_lower, "tickUpper": tick_upper, "poolID": pool_id, @@ -3258,14 +3249,14 @@ def decode_position_info(position_info: int) -> Dict: @staticmethod def encode_path_keys_input( - path: List[PoolKey], + path: list[PoolKey], currency_in: str, - hook_data_list: Optional[List[bytes]] = None, - ) -> List[PathKey]: + hook_data_list: list[bytes] | None = None, + ) -> list[PathKey]: """ Encodes a list of PoolKeys into the format expected by the quoter for multi-hop ExactInput quotes. """ - encoded_path: List[PathKey] = [] + encoded_path: list[PathKey] = [] if hook_data_list is None: hook_data_list = [b""] * len(path) else: @@ -3290,15 +3281,15 @@ def encode_path_keys_input( @staticmethod def encode_path_keys_output( - path: List[PoolKey], + path: list[PoolKey], currency_out: str, - hook_data_list: Optional[List[bytes]] = None, - ) -> List[PathKey]: + hook_data_list: list[bytes] | None = None, + ) -> list[PathKey]: """ Encodes a list of PoolKeys into the format expected by the quoter for multi-hop ExactOutput quotes. """ - encoded_path: List[PathKey] = [] + encoded_path: list[PathKey] = [] if hook_data_list is None: hook_data_list = [b""] * len(path) else: @@ -3353,15 +3344,15 @@ def get_token(self, address: AddressLike, abi_name: str = "erc20") -> ERC20Token _name = token_contract.functions.name().call() _symbol = token_contract.functions.symbol().call() decimals = token_contract.functions.decimals().call() - except Exception: + except ValueError: raise InvalidToken(address) try: name = _name.decode() - except Exception: + except (ValueError, AttributeError): name = str(_name) try: symbol = _symbol.decode() - except Exception as e: + except (ValueError, AttributeError) as e: logger.warning( "Error occurred while decoding symbol for %s: %s", _addr_to_str(address), @@ -3395,12 +3386,11 @@ def _deadline(self) -> int: return int(time.time()) + 10 * 60 def _get_tx_params( - self, value: int = 0, custom_nonce: Optional[Nonce] = None + self, value: int = 0, custom_nonce: Nonce | None = None ) -> TxParams: """Get generic transaction parameters.""" - if custom_nonce is not None: - if custom_nonce < Nonce(0): - raise ValueError("Nonce can only be a positive integer.") + if custom_nonce is not None and custom_nonce < Nonce(0): + raise ValueError("Nonce can only be a positive integer.") if not self.post_merge: return { "from": _addr_to_str(self.address), @@ -3428,8 +3418,8 @@ def _get_tx_params( def _build_and_send_tx( self, function: ContractFunction, - tx_params: Optional[TxParams] = None, - custom_nonce: Optional[Nonce] = None, + tx_params: TxParams | None = None, + custom_nonce: Nonce | None = None, ) -> HexBytes: """Build and send a transaction.""" if not tx_params: @@ -3439,7 +3429,7 @@ def _build_and_send_tx( transaction, private_key=self.private_key ) try: - return self.w3.eth.send_raw_transaction(signed_txn.rawTransaction) + return self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) finally: # logger.debug(f"nonce: {tx_params['nonce']}") if custom_nonce is None: diff --git a/uniswap/util.py b/uniswap/util.py index 221c0cea..cf7db0a9 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -1,30 +1,21 @@ -import functools +import asyncio import json import math import os +from collections.abc import Generator, Sequence from time import sleep from typing import ( Any, - Generator, - List, - Optional, - Sequence, - Tuple, - Union, ) from xml.etree import ElementTree as ET -import lru -from web3 import Web3 -from web3.contract import Contract +from web3 import AsyncWeb3, Web3 +from web3.contract import AsyncContract, Contract from web3.exceptions import NameNotFound -from web3.middleware.cache import construct_simple_cache_middleware -from web3.types import Middleware from .constants import ( MAX_TICK, MIN_TICK, - SIMPLE_CACHE_RPC_WHITELIST, _netid_to_name, _poolmanager_contract_addresses_v4, _tick_spacing, @@ -32,14 +23,7 @@ from .types import Address, AddressLike, PoolKey -def _get_eth_simple_cache_middleware() -> Middleware: - return construct_simple_cache_middleware( - cache=functools.partial(lru.LRU, 256), # type: ignore - rpc_whitelist=SIMPLE_CACHE_RPC_WHITELIST, - ) - - -def _str_to_addr(s: Union[AddressLike, str]) -> Address: +def _str_to_addr(s: AddressLike | str) -> Address: """Idempotent""" if isinstance(s, str): if s.startswith("0x"): @@ -62,7 +46,7 @@ def _addr_to_str(a: AddressLike) -> str: raise NameNotFound(a) -def is_same_address(a1: Union[AddressLike, str], a2: Union[AddressLike, str]) -> bool: +def is_same_address(a1: AddressLike | str, a2: AddressLike | str) -> bool: return bool(_str_to_addr(a1) == _str_to_addr(a2)) @@ -77,17 +61,23 @@ def _load_abi(name: str) -> str: return abi -@functools.lru_cache() def _load_contract(w3: Web3, abi_name: str, address: AddressLike) -> Contract: address = Web3.to_checksum_address(address) return w3.eth.contract(address=address, abi=_load_abi(abi_name)) +async def _load_contract_async( + w3: AsyncWeb3, abi_name: str, address: AddressLike +) -> AsyncContract: + address = Web3.to_checksum_address(address) + return w3.eth.contract(address=address, abi=_load_abi(abi_name)) + + def _load_contract_erc20(w3: Web3, address: AddressLike) -> Contract: return _load_contract(w3, "erc20", address) -def _encode_path(token_in: AddressLike, route: List[Tuple[int, AddressLike]]) -> bytes: +def _encode_path(token_in: AddressLike, route: list[tuple[int, AddressLike]]) -> bytes: """ Needed for multi-hop swaps in V3. @@ -222,7 +212,7 @@ def get_max_tick(fee: int) -> int: return (MAX_TICK // max_tick_spacing) * max_tick_spacing -def default_tick_range(fee: int) -> Tuple[int, int]: +def default_tick_range(fee: int) -> tuple[int, int]: min_tick = get_min_tick(fee) max_tick = get_max_tick(fee) @@ -270,14 +260,14 @@ def realised_fee_percentage(fee: int, amount_in: int) -> float: class V4pools: """Uniswap V4 pools handler""" - poolkeys_list: List[PoolKey] + poolkeys_list: list[PoolKey] def __init__( self, web3: Web3, ): """:param web3: Web3 instance connected to the network for which pool data is being fetched.""" - self.poolkeys_list: List[PoolKey] = list() + self.poolkeys_list: list[PoolKey] = [] self.web3 = web3 self.last_block = 0 @@ -294,11 +284,11 @@ def set_last_block(self, value: int) -> None: def fetch_poolkey_data( self, first_block: int, - chunk_size: int = 500, + chunk_size: int = 5, clear_list: bool = True, retry_attempts: int = 3, minutes_between_retries: int = 3, - last_block: Optional[int] = None, + last_block: int | None = None, ) -> int: """ :param first_block: Starting block for scanning process @@ -311,8 +301,8 @@ def fetch_poolkey_data( """ # Scans PoolManager contract' Initialize() event logs in order to get # list of all pools. See documentation for suggested starting blocks. - # chunk_size default value 500 should be suitable for public RPCs; - # can be increased on private/local RPCs for better performance. + # chunk_size default value 5 should be suitable for public RPCs; + # can be increased on paid or local RPCs for better performance. chain_id = int(self.web3.net.version) net_name = _netid_to_name[chain_id] @@ -340,7 +330,7 @@ def fetch_poolkey_data( if clear_list: self.poolkeys_list.clear() retry_attempts_done: int = 0 - for i in range(0, chunks_amount + 1): + for i in range(chunks_amount + 1): if start_block + chunk_size <= last_block_number: end_block = start_block + chunk_size else: @@ -351,16 +341,17 @@ def fetch_poolkey_data( flush=True, ) try: - logs = pool_manager_contract.events.Initialize().get_logs( # type: ignore [attr-defined] - fromBlock=start_block, toBlock=end_block + logs = pool_manager_contract.events.Initialize().get_logs( + from_block=start_block, to_block=end_block ) except Exception as e: # Exception occurs when chunk size value is too big so RPC endpoint rejects # requests OR RPC endpoint has issues. # In such cases, we will try to resume log retrieval process for a defined number of attempts. If all attempts fail, the method will be aborted and `-1`` value will be returned. + print(f"Error retrieving logs: {e}") while retry_attempts_done < retry_attempts: - print("") - print("") + print() + print() print( f"Error retrieving logs. Retrying. ({retry_attempts_done + 1}/{retry_attempts})" ) @@ -370,8 +361,226 @@ def fetch_poolkey_data( sleep(int(minutes_between_retries) * 60) retry_attempts_done += 1 try: - logs = pool_manager_contract.events.Initialize().get_logs( # type: ignore [attr-defined] - fromBlock=start_block, toBlock=end_block + logs = pool_manager_contract.events.Initialize().get_logs( + from_block=start_block, to_block=end_block + ) + print("Issue addressed. Resuming log retrieval.") + retry_attempts_done = 0 + break + except Exception as e_reconnect: + print(f"Attempt {retry_attempts_done} failed: {e_reconnect}") + if retry_attempts_done == retry_attempts: + print( + "Couldn't retrieve logs; check chunk size and RPC availability. Aborted. " + ) + print(f"Error details: {e}") + return -1 + for log_item in logs: + try: + pool_currency0 = str(log_item.args.currency0) + pool_currency1 = str(log_item.args.currency1) + pool_fee = int(str(log_item.args.fee)) + pool_tick_spacing = int(str(log_item.args.tickSpacing)) + pool_hooks = str(log_item.args.hooks) + pool: PoolKey = PoolKey( + pool_currency0, + pool_currency1, + pool_fee, + pool_tick_spacing, + pool_hooks, + ) + if pool not in self.poolkeys_list: + self.poolkeys_list.append(pool) + except AttributeError as e: + print(f"Error occurred while processing log item: {e}") + continue + self.set_last_block(end_block) + if end_block == last_block_number: + break + start_block = start_block + chunk_size + + print( + "---------------------------------------------------------------------------------------------" + ) + print(f"Logs processing completed. Last block processed {last_block_number}") + self.set_last_block(last_block_number) + return 0 + + def save_poolkeys_list(self, poolkey_data_filename: str) -> None: + """Saves poolKey list to specified file (XML format)""" + pool_data = ET.Element("PoolData") + + for pool_item in self.poolkeys_list: + pool = ET.SubElement(pool_data, "Pool") + + currency0 = ET.SubElement(pool, "Currency0") + currency0.text = str(pool_item.currency0) + + currency1 = ET.SubElement(pool, "Currency1") + currency1.text = str(pool_item.currency1) + + fee = ET.SubElement(pool, "Fee") + fee.text = str(pool_item.fee) + + tick_spacing = ET.SubElement(pool, "TickSpacing") + tick_spacing.text = str(pool_item.tick_spacing) + + hooks = ET.SubElement(pool, "Hooks") + hooks.text = str(pool_item.hooks) + + ET.ElementTree(pool_data).write(poolkey_data_filename) + + def load_poolkeys_list(self, poolkey_data_filename: str) -> None: + """Loads poolKey list from specified file (XML format)""" + if os.path.isfile(poolkey_data_filename): + try: + tree = ET.parse(poolkey_data_filename) + except ET.ParseError: + raise ValueError( + "Parse error, file seems to be corrupted (" + + poolkey_data_filename + + ")" + ) + self.poolkeys_list.clear() + root = tree.getroot() + for item in root: + pool_currency0 = str(item[0].text) + pool_currency1 = str(item[1].text) + pool_fee = int(str(item[2].text)) + pool_tick_spacing = int(str(item[3].text)) + pool_hooks = str(item[4].text) + pool: PoolKey = PoolKey( + pool_currency0, + pool_currency1, + pool_fee, + pool_tick_spacing, + pool_hooks, + ) + self.poolkeys_list.append(pool) + else: + raise ValueError("Couldn't locate file " + poolkey_data_filename) + + def get_poolkeys_sublist(self, currency0: str, currency1: str) -> list[PoolKey]: + """Returns all pools for the (currency0, currency1) pair""" + if currency0.lower() < currency1.lower(): + c0, c1 = currency0.lower(), currency1.lower() + else: + c0, c1 = currency1.lower(), currency0.lower() + result_list = [ + x + for x in self.poolkeys_list + if c0 == x.currency0.lower() and c1 == x.currency1.lower() + ] + return result_list + + +class AsyncV4pools: + """Uniswap V4 pools handler""" + + poolkeys_list: list[PoolKey] + + def __init__( + self, + web3: AsyncWeb3, + ): + """:param web3: Web3 instance connected to the network for which pool data is being fetched.""" + self.poolkeys_list: list[PoolKey] = [] + self.web3 = web3 + self.last_block = 0 + + def get_last_block( + self, + ) -> int: + """Returns last block number processed by fetch_poolkey_data() method.""" + return self.last_block + + def set_last_block(self, value: int) -> None: + """Sets last block number processed by fetch_poolkey_data() method.""" + self.last_block = value + + async def fetch_poolkey_data( + self, + first_block: int, + chunk_size: int = 5, + clear_list: bool = True, + retry_attempts: int = 3, + minutes_between_retries: int = 3, + last_block: int | None = None, + ) -> int: + """ + :param first_block: Starting block for scanning process + :param chunk_size: Defines amount of blocks per single log request + :param clear_list: When True, clears pool list before log scanning, when False - new entries will be added to the end of the list. + :param retry_attempts: Number of attempts to retry and resume log retrieval in case of RPC returns errors like `500` etc. + :param minutes_between_retries: Minutes to wait between retry attempts. + :param last_block: Optional parameter defining the last block for scanning process. If None, current block number will be used. + :return: 0 if logs were successfully processed, -1 if logs retrieval failed (e.g. due to wrong chunk size or RPC endpoint failure). + """ + # Scans PoolManager contract' Initialize() event logs in order to get + # list of all pools. See documentation for suggested starting blocks. + # chunk_size default value 5 should be suitable for public RPCs; + # can be increased on paid or local RPCs for better performance. + + chain_id: int = int(await self.web3.net.version) + net_name = _netid_to_name[chain_id] + pool_manager_contract_address = _poolmanager_contract_addresses_v4[net_name] + pool_manager_contract = await _load_contract_async( + self.web3, + "uniswap-v4/poolmanager", + _str_to_addr(pool_manager_contract_address), + ) + first_block_number: int = first_block + if last_block is None: + last_block_number: int = int(await self.web3.eth.get_block_number()) + else: + last_block_number = min( + max(first_block, last_block), + int(await self.web3.eth.get_block_number()), + ) + + chunks_amount = int((last_block_number - first_block_number) // chunk_size) + start_block = first_block_number + end_block = 0 + + print( + f"Logs processing started, start block = {start_block}; end block = {last_block_number}." + ) + if clear_list: + self.poolkeys_list.clear() + retry_attempts_done: int = 0 + for i in range(chunks_amount + 1): + if start_block + chunk_size <= last_block_number: + end_block = start_block + chunk_size + else: + end_block = last_block_number + print( + f"Processing chunk {i}/{chunks_amount}; (start block = {start_block}; end block = {end_block})", + end="\r", + flush=True, + ) + try: + logs = await pool_manager_contract.events.Initialize().get_logs( + from_block=start_block, to_block=end_block + ) + except Exception as e: + # Exception occurs when chunk size value is too big so RPC endpoint rejects + # requests OR RPC endpoint has issues. + # In such cases, we will try to resume log retrieval process for a defined number of attempts. If all attempts fail, the method will be aborted and `-1`` value will be returned. + print(f"Error retrieving logs: {e}") + while retry_attempts_done < retry_attempts: + print() + print() + print( + f"Error retrieving logs. Retrying. ({retry_attempts_done + 1}/{retry_attempts})" + ) + print( + f"Waiting for {minutes_between_retries} minutes before next attempt..." + ) + await asyncio.sleep(int(minutes_between_retries) * 60) + retry_attempts_done += 1 + try: + logs = await pool_manager_contract.events.Initialize().get_logs( + from_block=start_block, to_block=end_block ) print("Issue addressed. Resuming log retrieval.") retry_attempts_done = 0 @@ -438,7 +647,6 @@ def save_poolkeys_list(self, poolkey_data_filename: str) -> None: hooks.text = str(pool_item.hooks) ET.ElementTree(pool_data).write(poolkey_data_filename) - return def load_poolkeys_list(self, poolkey_data_filename: str) -> None: """Loads poolKey list from specified file (XML format)""" @@ -470,7 +678,7 @@ def load_poolkeys_list(self, poolkey_data_filename: str) -> None: else: raise ValueError("Couldn't locate file " + poolkey_data_filename) - def get_poolkeys_sublist(self, currency0: str, currency1: str) -> List[PoolKey]: + def get_poolkeys_sublist(self, currency0: str, currency1: str) -> list[PoolKey]: """Returns all pools for the (currency0, currency1) pair""" if currency0.lower() < currency1.lower(): c0, c1 = currency0.lower(), currency1.lower()