pax_global_header00006660000000000000000000000064152716170010014512gustar00rootroot0000000000000052 comment=612e0d09da86ab0d65fe2d349d9344979779c647 mosquito-aiormq-612e0d0/000077500000000000000000000000001527161700100152035ustar00rootroot00000000000000mosquito-aiormq-612e0d0/.editorconfig000066400000000000000000000004351527161700100176620ustar00rootroot00000000000000root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true [*.{py,yml}] indent_style = space [*.py] indent_size = 4 [docs/**.py] max_line_length = 80 [*.rst] indent_size = 4 [Makefile] indent_style = tab [*.yml] indent_size = 2 mosquito-aiormq-612e0d0/.github/000077500000000000000000000000001527161700100165435ustar00rootroot00000000000000mosquito-aiormq-612e0d0/.github/workflows/000077500000000000000000000000001527161700100206005ustar00rootroot00000000000000mosquito-aiormq-612e0d0/.github/workflows/publish.yml000066400000000000000000000013351527161700100227730ustar00rootroot00000000000000name: Publish to PyPI on: release: types: [published] env: FORCE_COLOR: 1 jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v10.2.0 - name: Set version from release tag run: uv version "${GITHUB_REF_NAME#v}" - name: Build package run: uv build - uses: actions/upload-artifact@v7 with: name: dist path: dist/ publish: runs-on: ubuntu-latest needs: build environment: pypi permissions: id-token: write steps: - uses: actions/download-artifact@v8 with: name: dist path: dist/ - uses: pypa/gh-action-pypi-publish@v1.14.2 mosquito-aiormq-612e0d0/.github/workflows/tests.yml000066400000000000000000000063431527161700100224730ustar00rootroot00000000000000name: tests on: push: branches: [ master ] pull_request: branches: [ master ] jobs: ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Setup python3.11 uses: actions/setup-python@v7 with: python-version: "3.11" - uses: astral-sh/setup-uv@v10.2.0 - run: uv sync - run: uv run ruff check env: FORCE_COLOR: 1 ty: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Setup python3.11 uses: actions/setup-python@v7 with: python-version: "3.11" - uses: astral-sh/setup-uv@v10.2.0 - run: uv sync - run: uv run ty check env: FORCE_COLOR: 1 docs: runs-on: ubuntu-latest services: rabbitmq: image: docker://mosquito/aiormq-rabbitmq ports: - 5671:5671 - 5672:5672 - 15671:15671 - 15672:15672 steps: - uses: actions/checkout@v7 - name: Setup python3.11 uses: actions/setup-python@v7 with: python-version: "3.11" - uses: astral-sh/setup-uv@v10.2.0 - run: uv sync - run: uv run pytest -svv README.md env: AMQP_URL: amqp://guest:guest@localhost// FORCE_COLOR: 1 tests: runs-on: ubuntu-latest services: rabbitmq: image: docker://mosquito/aiormq-rabbitmq ports: - 5671:5671 - 5672:5672 - 15671:15671 - 15672:15672 strategy: fail-fast: false # The uvloop wheels start at cp314t, therefore the 3.13 free-threaded # build runs on the asyncio event loop. matrix: include: - python: '3.11' loop: uvloop - python: '3.12' loop: uvloop - python: '3.13' loop: uvloop - python: '3.13t' loop: asyncio - python: '3.14' loop: uvloop - python: '3.14t' loop: uvloop - python: '3.15' loop: uvloop - python: '3.15t' loop: uvloop steps: - uses: actions/checkout@v7 - name: Setup python${{ matrix.python }} uses: actions/setup-python@v7 with: python-version: "${{ matrix.python }}" allow-prereleases: true - uses: astral-sh/setup-uv@v10.2.0 - run: >- uv sync --no-default-groups --group dev --group ${{ matrix.loop }} - name: pytest run: >- uv run pytest \ -vv \ --cov=aiormq \ --cov-report=term-missing \ --doctest-modules \ --aiomisc-test-timeout=120 \ tests env: AMQP_URL: amqp://guest:guest@localhost// FORCE_COLOR: 1 - run: uv run coveralls env: COVERALLS_PARALLEL: 'true' COVERALLS_SERVICE_NAME: github GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} finish: needs: - tests runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@v2.3.8 with: github-token: ${{ secrets.github_token }} parallel-finished: true mosquito-aiormq-612e0d0/.gitignore000066400000000000000000000037371527161700100172050ustar00rootroot00000000000000# Created by .ignore support plugin (hsz.mobi) ### VirtualEnv template # Virtualenv # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ .Python [Bb]in [Ii]nclude [Ll]ib [Ll]ib64 [Ll]ocal [Ss]cripts pyvenv.cfg .venv pip-selfcheck.json ### IPythonNotebook template # Temporary data .ipynb_checkpoints/ ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ docs/source/apidoc # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # pytest .pytest_cache # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: .idea/ ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties /htmlcov /temp .DS_Store .*cache mosquito-aiormq-612e0d0/COPYING000066400000000000000000000243301527161700100162400ustar00rootroot00000000000000Apache License ============== _Version 2.0, January 2004_ ### Terms and Conditions for use, reproduction, and distribution #### 1. Definitions “License” shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. “Licensor” shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. “Legal Entity” shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, “control” means **(i)** the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the outstanding shares, or **(iii)** beneficial ownership of such entity. “You” (or “Your”) shall mean an individual or Legal Entity exercising permissions granted by this License. “Source” form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. “Object” form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. “Work” shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). “Derivative Works” shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. “Contribution” shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as “Not a Contribution.” “Contributor” shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. #### 2. Grant of Copyright License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. #### 3. Grant of Patent License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. #### 4. Redistribution You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: * **(a)** You must give any other recipients of the Work or Derivative Works a copy of this License; and * **(b)** You must cause any modified files to carry prominent notices stating that You changed the files; and * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. #### 5. Submission of Contributions Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. #### 6. Trademarks This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. #### 7. Disclaimer of Warranty Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. #### 8. Limitation of Liability In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. #### 9. Accepting Warranty or Additional Liability While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. _END OF TERMS AND CONDITIONS_ ### APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets `[]` replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same “printed page” as the copyright notice for easier identification within third-party archives. Copyright 2023 Dmitry Orlov Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. mosquito-aiormq-612e0d0/Makefile000066400000000000000000000025101527161700100166410ustar00rootroot00000000000000all: clean test RABBITMQ_CONTAINER_NAME:=aiormq_rabbitmq RABBITMQ_IMAGE:=mosquito/aiormq-rabbitmq RABBITMQ_IMAGE_TAG:=4.4 RABBITMQ_IMAGE_PLATFORMS:=linux/amd64,linux/arm64 RABBITMQ_BUILDER:=aiormq # Builds the test broker image for the host architecture only. rabbitmq-image: docker build -f tests/certs/Dockerfile \ -t $(RABBITMQ_IMAGE):$(RABBITMQ_IMAGE_TAG) \ -t $(RABBITMQ_IMAGE):latest . # A multi-platform build needs the docker-container driver. rabbitmq-builder: docker buildx inspect $(RABBITMQ_BUILDER) > /dev/null 2>&1 || \ docker buildx create --name $(RABBITMQ_BUILDER) \ --driver docker-container # Builds and pushes the image for every platform. CI runs on linux/amd64. rabbitmq-image-push: rabbitmq-builder docker buildx build --builder $(RABBITMQ_BUILDER) \ --platform $(RABBITMQ_IMAGE_PLATFORMS) \ -f tests/certs/Dockerfile \ -t $(RABBITMQ_IMAGE):$(RABBITMQ_IMAGE_TAG) \ -t $(RABBITMQ_IMAGE):latest \ --push . rabbitmq: docker kill $(RABBITMQ_CONTAINER_NAME) || true docker run --pull=always --rm -d \ --name $(RABBITMQ_CONTAINER_NAME) \ -p 5671:5671 \ -p 5672:5672 \ -p 15671:15671 \ -p 15672:15672 \ $(RABBITMQ_IMAGE) upload: uv build uv publish test: uv run pytest -vvx --cov=aiormq \ --cov-report=term-missing tests README.md clean: rm -fr *.egg-info .tox develop: clean uv sync mosquito-aiormq-612e0d0/README.md000066400000000000000000000626121527161700100164710ustar00rootroot00000000000000# AIORMQ [![Coveralls](https://coveralls.io/repos/github/mosquito/aiormq/badge.svg?branch=master)](https://coveralls.io/github/mosquito/aiormq?branch=master) [![Status](https://img.shields.io/pypi/status/aiormq.svg)](https://github.com/mosquito/aiormq) [![Build status](https://github.com/mosquito/aiormq/workflows/tests/badge.svg)](https://github.com/mosquito/aiormq/actions?query=workflow%3Atests) [![Latest Version](https://img.shields.io/pypi/v/aiormq.svg)](https://pypi.python.org/pypi/aiormq/) [![Wheel](https://img.shields.io/pypi/wheel/aiormq.svg)](https://pypi.python.org/pypi/aiormq/) [![Python versions](https://img.shields.io/pypi/pyversions/aiormq.svg)](https://pypi.python.org/pypi/aiormq/) [![License](https://img.shields.io/pypi/l/aiormq.svg)](https://github.com/mosquito/aiormq/blob/master/LICENSE.md) aiormq is a pure python AMQP client library. ## Table of contents * [Status](#status) * [Features](#features) * [Tutorial](#tutorial) * [Introduction](#introduction) * [Connection loss and reconnecting](#connection-loss-and-reconnecting) * [Work Queues](#work-queues) * [Publish Subscribe](#publish-subscribe) * [Routing](#routing) * [Topics](#topics) * [Consumer cancelled by the broker](#consumer-cancelled-by-the-broker) * [Remote procedure call (RPC)](#remote-procedure-call-rpc) ## Status * 3.x.x branch - Production/Stable * 4.x.x branch - Unstable (Experimental) * 5.x.x and greater is only Production/Stable releases. ## Features * Connecting by URL * amqp example: **amqp://user:password@server.host/vhost** * secure amqp example: **amqps://user:password@server.host/vhost?cafile=ca.pem&keyfile=key.pem&certfile=cert.pem&no_verify_ssl=0** * Buffered queue for received frames * Only [PLAIN](https://www.rabbitmq.com/authentication.html) auth mechanism support * [Publisher confirms](https://www.rabbitmq.com/confirms.html) support * [Transactions](https://www.rabbitmq.com/semantics.html) support * Channel based asynchronous locks > **Note** > AMQP 0.9.1 requires serialize sending for some frame types > on the channel. e.g. Content body must be following after > content header. But frames might be sent asynchronously > on another channels. * Tracking unroutable messages (Use **connection.channel(on_return_raises=False)** for disabling) * Full SSL/TLS support, using your choice of: * `amqps://` url query parameters: * `cafile=` - string contains path to ca certificate file * `capath=` - string contains path to ca certificates * `cadata=` - base64 encoded ca certificate data * `keyfile=` - string contains path to key file * `certfile=` - string contains path to certificate file * `no_verify_ssl` - boolean disables certificates validation * `context=` [SSLContext](https://docs.python.org/3/library/ssl.html#ssl.SSLContext) keyword argument to `connect()`. * Python [type hints](https://docs.python.org/3/library/typing.html) * Uses [pamqp](https://pypi.org/project/pamqp/) as an AMQP 0.9.1 frame encoder/decoder ## Tutorial In the examples below `amqp_url` is a connection URL string such as `amqp://guest:guest@localhost/`. The examples run inside a coroutine, so `await` is used at the top level. `aiormq.connect()` prepares a connection without opening it. `async with aiormq.connect(url) as connection:` opens the connection and closes it on exit. `await aiormq.connect(url)` from older versions still works. ### Introduction #### Simple consumer ```python import asyncio import aiormq async def on_message(message): """ on_message doesn't necessarily have to be defined as async. Here it is to show that it's possible. """ print(f" [x] Received message {message!r}") print(f"Message body is: {message.body!r}") print("Before sleep!") await asyncio.sleep(1) # Represents async I/O operations print("After sleep!") # Perform connection async with aiormq.connect(amqp_url) as connection: # Creating a channel channel = await connection.channel() # Declaring queue declare_ok = await channel.queue_declare('hello', auto_delete=True) consume_ok = await channel.basic_consume( declare_ok.queue, on_message, no_ack=True ) ``` ```python # Keep consuming until the connection closes and report its cause. try: await connection.closing except aiormq.AMQPConnectionError as exc: print(f"Connection lost: {exc}") ``` Connection failures happen in background tasks. Await `connection.closing` to receive their close reason in your application. Each access while the connection is open returns an independent observer, so cancelling this wait does not cancel the connection's close state. The connection context manager handles cleanup when the block exits. See [Connection loss and reconnecting](#connection-loss-and-reconnecting) for heartbeat cancellation, publication failures and recovery. #### Simple publisher ```python import aiormq body = b'Hello World!' # Perform connection async with aiormq.connect(amqp_url) as connection: # Creating a channel channel = await connection.channel() declare_ok = await channel.queue_declare("hello", auto_delete=True) # Sending the message await channel.basic_publish( body, routing_key='hello', mandatory=True, timeout=10, ) print(f" [x] Sent {body}") message = await channel.basic_get(declare_ok.queue) print(f" [x] Received message from {declare_ok.queue!r}") assert message is not None assert message.routing_key == "hello" assert message.body == b'Hello World!' ``` ### Connection loss and reconnecting #### Detecting closure Both connections and channels expose a `closing` future. Await it to observe closure, or register a synchronous callback with `connection.closing.add_done_callback(callback)`. The callback receives the future; call its `result()` to retrieve the close reason, handling exceptions. A channel can close while the connection and its other channels remain usable, so watch `channel.closing` too when your application depends on that channel. A broker shutdown normally raises `ConnectionClosed` (an `AMQPConnectionError`); a broken transport can raise another `AMQPConnectionError`. Heartbeat expiry and the default `close()` can instead produce `asyncio.CancelledError`. On a connection, `close(exc=None)` permits a normal result. Do not use log messages or private reader/writer attributes as a connection health API. `is_closed` is a snapshot; the connection can fail immediately after the check. Pamqp protocol exceptions during frame decoding become `aiormq.InvalidFrameError`, with the original exception available as `__cause__`. Unexpected handshake frames and protocol headers raise `InvalidFrameError` or `ProtocolSyntaxError`. Both belong to `aiormq.AMQPError`, so one handler can catch them together with other AMQP errors during connection setup or when awaiting `connection.closing`. The helper below reports closure while preserving cancellation of the task that is waiting. It does not retry operations: ```python import asyncio import aiormq async def observe_closure(connection): try: await connection.closing except asyncio.CancelledError as exc: if asyncio.current_task().cancelling(): raise # The application is stopping this task. return exc # Cancellation reported by the connection itself. except aiormq.AMQPError as exc: return exc return None ``` Call `reason = await observe_closure(connection)` on an open connection. Use `async with aiormq.connect(url)` or `try/finally` with `await connection.close()` to release resources when your task exits. Cancelling an observer only stops that observation; it does not close the connection. Closure notifications are not a substitute for awaiting cleanup. #### Publishing during a disconnect `basic_publish()` uses publisher confirms by default. With confirms enabled, it waits for the broker's confirmation; this does not mean that a consumer has processed the message. Set `timeout=10`, for example, to bound the operation, including waiting for the channel lock, outgoing queue and confirmation. Expiry raises `TimeoutError`. Heartbeats help detect a dead peer, but do not replace an operation timeout. Pending publications can fail when their channel or connection closes. Handle errors around the awaited publication as well as observing `closing`. Cancellation can also propagate from connection shutdown or from your own task. Calls made after closure can raise `ChannelInvalidStateError`. With `publisher_confirms=False`, a successful return provides no broker confirmation. `wait=False` only skips waiting for the local write buffer to drain; it does **not** disable publisher confirms. Use `mandatory=True` and the default `on_return_raises=True` to receive `PublishError` for an unroutable message; routing failure is separate from a disconnect. A broker `Basic.Nack` raises `DeliveryError`. Its text includes the delivery sequence number and whether the reply covers multiple publications; `error.frame` retains the original frame. A Nack carries no reason code or explanation, so inspect broker logs and queue policies for the cause. For example, a full queue configured with `overflow=reject-publish` can reject new publications. See [queue overflow behaviour](https://www.rabbitmq.com/docs/maxlength#queue-overflow-behaviour). `PublishError` is a subclass of `DeliveryError` for returned messages. Its text includes `Basic.Return`'s reply code, reply text, exchange and routing key; `error.message` contains the returned message. A Nack's delivery tag is local to the publishing channel, not an application message identifier. Decide whether to retry based on the failure and application requirements; aiormq does not retry automatically. A timeout or lost connection does not establish whether the broker accepted the publication. Retrying an unconfirmed message can produce a duplicate if only the confirmation was lost. Preserve an application message identifier across retries and make processing idempotent or deduplicate in the consumer; setting `message_id` alone does not make RabbitMQ deduplicate messages. See [RabbitMQ's reliability guide](https://www.rabbitmq.com/docs/reliability#data-safety-on-the-publisher-side). #### Restoring consumers and publishers aiormq does not reconnect or replay publications automatically. After a connection failure, create a **new connection**, then recreate channels, exchange/queue declarations, bindings, QoS settings and consumer registrations. If only one channel failed, recreate that channel on the existing connection after addressing the cause. Old channel objects and delivery tags cannot be used on the replacement channel. Use a retry delay with backoff rather than reconnecting in a tight loop, and allow task cancellation to stop the retry loop. Decide separately which unconfirmed publications to retry. For automatic connection and topology recovery, use [aio-pika's `connect_robust()`](https://docs.aio-pika.com/quick-start.html). Recovery does not remove the need to handle uncertain publication outcomes. With manual acknowledgements, unacknowledged deliveries on a closed channel can be redelivered while the queue still exists. Expect consumer callbacks to be cancelled during shutdown; do not acknowledge their old delivery tags on a new channel. With `no_ack=True`, RabbitMQ does not wait for processing to finish and cannot recover a delivery lost by the consumer. Exclusive and auto-delete queues may disappear when their connection or consumers go away; plan their recreation and message retention accordingly. ### Work Queues #### Create new task ```python import aiormq # Perform connection async with aiormq.connect(amqp_url) as connection: # Creating a channel channel = await connection.channel() body = b"Hello World!" # Sending the message await channel.basic_publish( body, routing_key='task_queue', properties=aiormq.spec.Basic.Properties( delivery_mode=1, ) ) print(f" [x] Sent {body!r}") ``` #### Simple worker ```python import aiormq import aiormq.abc async def on_message(message: aiormq.abc.DeliveredMessage): print(f" [x] Received message {message!r}") print(f" Message body is: {message.body!r}") # Perform connection async with aiormq.connect(amqp_url) as connection: # Creating a channel channel = await connection.channel() await channel.basic_qos(prefetch_count=1) # Declaring queue declare_ok = await channel.queue_declare('task_queue', durable=True) # Start listening the queue with name 'task_queue' await channel.basic_consume(declare_ok.queue, on_message, no_ack=True) print(" [*] Waiting for messages.") # The connection stays open while this block runs. ``` ### Publish Subscribe #### Publisher ```python import aiormq # Perform connection async with aiormq.connect(amqp_url) as connection: # Creating a channel channel = await connection.channel() await channel.exchange_declare( exchange='logs', exchange_type='fanout' ) body = b"Hello World!" # Sending the message await channel.basic_publish( body, routing_key='info', exchange='logs' ) print(f" [x] Sent {body!r}") ``` #### Subscriber ```python import aiormq import aiormq.abc async def on_message(message: aiormq.abc.DeliveredMessage): print(f"[x] {message.body!r}") await message.channel.basic_ack( message.delivery.delivery_tag ) # Perform connection async with aiormq.connect(amqp_url) as connection: # Creating a channel channel = await connection.channel() await channel.basic_qos(prefetch_count=1) await channel.exchange_declare( exchange='logs', exchange_type='fanout' ) # Declaring queue declare_ok = await channel.queue_declare(exclusive=True) # Binding the queue to the exchange await channel.queue_bind(declare_ok.queue, 'logs') # Start listening the queue await channel.basic_consume(declare_ok.queue, on_message) print(' [*] Waiting for logs.') # The connection stays open while this block runs. ``` ### Routing #### Direct consumer ```python import aiormq import aiormq.abc async def on_message(message: aiormq.abc.DeliveredMessage): print(f" [x] {message.delivery.routing_key!r}:{message.body!r}") await message.channel.basic_ack( message.delivery.delivery_tag ) # Perform connection async with aiormq.Connection(amqp_url) as connection: # Creating a channel channel = await connection.channel() await channel.basic_qos(prefetch_count=1) severities = ["info", "warning", "error"] # Declare an exchange await channel.exchange_declare( exchange='direct_logs', exchange_type='direct' ) # Declaring random queue declare_ok = await channel.queue_declare(durable=True, auto_delete=True) for severity in severities: await channel.queue_bind( declare_ok.queue, 'direct_logs', routing_key=severity ) # Start listening the random queue await channel.basic_consume(declare_ok.queue, on_message) print(" [*] Waiting for messages.") # The connection stays open while this block runs. ``` #### Emitter ```python import aiormq # Perform connection async with aiormq.connect(amqp_url) as connection: # Creating a channel channel = await connection.channel() await channel.exchange_declare( exchange='direct_logs', exchange_type='direct' ) routing_key = 'info' body = b"Hello World!" # Sending the message await channel.basic_publish( body, exchange='direct_logs', routing_key=routing_key, properties=aiormq.spec.Basic.Properties( delivery_mode=1 ) ) print(f" [x] Sent {body!r}") ``` ### Topics #### Publisher ```python import aiormq # Perform connection async with aiormq.connect(amqp_url) as connection: # Creating a channel channel = await connection.channel() await channel.exchange_declare('topic_logs', exchange_type='topic') routing_key = 'anonymous.info' body = b"Hello World!" # Sending the message await channel.basic_publish( body, exchange='topic_logs', routing_key=routing_key, properties=aiormq.spec.Basic.Properties( delivery_mode=1 ) ) print(f" [x] Sent {body!r}") ``` #### Consumer ```python import aiormq import aiormq.abc async def on_message(message: aiormq.abc.DeliveredMessage): print(f" [x] {message.delivery.routing_key!r}:{message.body!r}") await message.channel.basic_ack( message.delivery.delivery_tag ) # Perform connection async with aiormq.connect(amqp_url) as connection: # Creating a channel channel = await connection.channel() await channel.basic_qos(prefetch_count=1) # Declare an exchange await channel.exchange_declare('topic_logs', exchange_type='topic') # Declaring queue declare_ok = await channel.queue_declare(exclusive=True) binding_keys = ["*.info", "kern.*"] for binding_key in binding_keys: await channel.queue_bind( declare_ok.queue, 'topic_logs', routing_key=binding_key ) # Start listening the queue await channel.basic_consume(declare_ok.queue, on_message) print(" [*] Waiting for messages.") # The connection stays open while this block runs. ``` ### Consumer cancelled by the broker The broker cancels a consumer when its queue is deleted or when a cluster node that hosts the queue goes away. Register a callback in `channel.on_consumer_cancel_callbacks` to get the `Basic.Cancel` frame and react, for example by consuming again or by stopping the application. ```python import asyncio import aiormq async def on_message(message): print(f" [x] Received message {message.body!r}") cancelled = asyncio.get_running_loop().create_future() def on_consumer_cancel(frame: aiormq.spec.Basic.Cancel): print(f" [!] Consumer {frame.consumer_tag!r} cancelled by the broker") cancelled.set_result(frame.consumer_tag) async with aiormq.connect(amqp_url) as connection: channel = await connection.channel() channel.on_consumer_cancel_callbacks.add(on_consumer_cancel) declare_ok = await channel.queue_declare('cancel_me', auto_delete=True) consume_ok = await channel.basic_consume(declare_ok.queue, on_message) # Deleting the queue makes the broker cancel the consumer. await channel.queue_delete(declare_ok.queue) assert await cancelled == consume_ok.consumer_tag ``` A RabbitMQ 4.3 or later broker also cancels a consumer which does not acknowledge a delivery in time, and it keeps the consumer tag until the client answers with `Basic.CancelOk`. aiormq sends that answer before the callbacks run, when the broker advertises the `accept_consumer_cancel_ok` capability (`connection.accept_consumer_cancel_ok`). A consumer which starts again with the cancelled tag therefore reaches the broker after the answer, and the broker accepts the tag. > **Note** > The channel reader waits for the callbacks. A callback which calls the > broker, for example `basic_consume`, must run that call as a separate > task, otherwise it waits for its own reply forever. ### Remote procedure call (RPC) #### RPC server ```python import aiormq import aiormq.abc def fib(n): if n == 0: return 0 elif n == 1: return 1 else: return fib(n-1) + fib(n-2) async def on_message(message: aiormq.abc.DeliveredMessage): n = int(message.body.decode()) print(f" [.] fib({n})") response = str(fib(n)).encode() await message.channel.basic_publish( response, routing_key=message.header.properties.reply_to, properties=aiormq.spec.Basic.Properties( correlation_id=message.header.properties.correlation_id ), ) await message.channel.basic_ack(message.delivery.delivery_tag) print('Request complete') # Perform connection server_connection = aiormq.Connection(amqp_url) await server_connection.connect() # Creating a channel server_channel = await server_connection.channel() # Declaring queue declare_ok = await server_channel.queue_declare('rpc_queue', auto_delete=True) # Start listening the queue with name 'rpc_queue' await server_channel.basic_consume(declare_ok.queue, on_message) print(" [x] Awaiting RPC requests") ``` #### RPC client ```python import asyncio import uuid import aiormq import aiormq.abc class FibonacciRpcClient: def __init__(self): self.connection = None # type: aiormq.Connection self.channel = None # type: aiormq.Channel self.callback_queue = '' self.futures = {} async def connect(self): self.connection = aiormq.Connection(amqp_url) await self.connection.connect() self.channel = await self.connection.channel() declare_ok = await self.channel.queue_declare( exclusive=True, auto_delete=True ) await self.channel.basic_consume(declare_ok.queue, self.on_response) self.callback_queue = declare_ok.queue return self async def on_response(self, message: aiormq.abc.DeliveredMessage): future = self.futures.pop(message.header.properties.correlation_id) future.set_result(message.body) async def call(self, n): correlation_id = str(uuid.uuid4()) future = asyncio.get_running_loop().create_future() self.futures[correlation_id] = future await self.channel.basic_publish( str(n).encode(), routing_key='rpc_queue', properties=aiormq.spec.Basic.Properties( content_type='text/plain', correlation_id=correlation_id, reply_to=self.callback_queue, ) ) return int(await future) fibonacci_rpc = await FibonacciRpcClient().connect() print(" [x] Requesting fib(30)") response = await fibonacci_rpc.call(30) print(f" [.] Got {response!r}") await fibonacci_rpc.connection.close() ``` mosquito-aiormq-612e0d0/aiormq/000077500000000000000000000000001527161700100164735ustar00rootroot00000000000000mosquito-aiormq-612e0d0/aiormq/__init__.py000066400000000000000000000034231527161700100206060ustar00rootroot00000000000000from pamqp import commands as spec from . import abc from .channel import Channel from .connection import ( Connection, connect, SSLContextProvider, TransportFactory, ) from .exceptions import ( AMQPChannelError, AMQPConnectionError, AMQPError, AMQPException, AuthenticationError, ChannelAccessRefused, ChannelClosed, ChannelInvalidStateError, ChannelLockedResource, ChannelNotFoundEntity, ChannelPreconditionFailed, ConnectionChannelError, ConnectionClosed, ConnectionCommandInvalid, ConnectionFrameError, ConnectionInternalError, ConnectionNotAllowed, ConnectionNotImplemented, ConnectionResourceError, ConnectionSyntaxError, ConnectionUnexpectedFrame, DeliveryError, DuplicateConsumerTag, IncompatibleProtocolError, InvalidFrameError, MethodNotImplemented, ProbableAuthenticationError, ProtocolSyntaxError, PublishError, ) __all__ = ( "AMQPChannelError", "AMQPConnectionError", "AMQPError", "AMQPException", "AuthenticationError", "Channel", "ChannelAccessRefused", "ChannelClosed", "ChannelInvalidStateError", "ChannelLockedResource", "ChannelNotFoundEntity", "ChannelPreconditionFailed", "Connection", "ConnectionChannelError", "ConnectionClosed", "ConnectionCommandInvalid", "SSLContextProvider", "TransportFactory", "ConnectionFrameError", "ConnectionInternalError", "ConnectionNotAllowed", "ConnectionNotImplemented", "ConnectionResourceError", "ConnectionSyntaxError", "ConnectionUnexpectedFrame", "DeliveryError", "DuplicateConsumerTag", "IncompatibleProtocolError", "InvalidFrameError", "MethodNotImplemented", "ProbableAuthenticationError", "ProtocolSyntaxError", "PublishError", "abc", "connect", "spec", ) mosquito-aiormq-612e0d0/aiormq/abc.py000066400000000000000000000560111527161700100175750ustar00rootroot00000000000000import asyncio import dataclasses import io import logging from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable, Coroutine, Iterable from types import TracebackType from typing import Any, Literal, overload import pamqp from pamqp import commands as spec from pamqp.base import Frame from pamqp.body import ContentBody from pamqp.commands import Basic, Channel, Confirm, Exchange, Queue, Tx from pamqp.common import FieldArray, FieldTable, FieldValue from pamqp.constants import REPLY_SUCCESS from pamqp.header import ContentHeader from pamqp.heartbeat import Heartbeat from yarl import URL ExceptionType = BaseException | type[BaseException] # noinspection PyShadowingNames class TaskWrapper: __slots__ = "_exception", "task" _exception: BaseException | type[BaseException] task: asyncio.Task def __init__(self, task: asyncio.Task): self.task = task self._exception = asyncio.CancelledError def throw(self, exception: ExceptionType) -> None: self._exception = exception self.task.cancel(exception) async def __inner(self) -> Any: try: return await self.task except asyncio.CancelledError as e: if ( isinstance(self._exception, BaseException) and self._exception.__cause__ is not None ): raise self._exception raise self._exception from e def __await__(self, *args: Any, **kwargs: Any) -> Any: return self.__inner().__await__() def cancel(self) -> None: return self.throw(asyncio.CancelledError()) def __getattr__(self, item: str) -> Any: return getattr(self.task, item) def __repr__(self) -> str: return "<%s: %s>" % (self.__class__.__name__, repr(self.task)) TaskType = asyncio.Task | TaskWrapper CoroutineType = Coroutine[Any, None, Any] GetResultType = Basic.GetEmpty | Basic.GetOk @dataclasses.dataclass(frozen=True) class DeliveredMessage: delivery: spec.Basic.Deliver | spec.Basic.Return | GetResultType header: ContentHeader body: bytes channel: "AbstractChannel" @property def routing_key(self) -> str | None: if isinstance( self.delivery, ( spec.Basic.Return, spec.Basic.GetOk, spec.Basic.Deliver, ), ): return self.delivery.routing_key return None @property def exchange(self) -> str | None: if isinstance( self.delivery, ( spec.Basic.Return, spec.Basic.GetOk, spec.Basic.Deliver, ), ): return self.delivery.exchange return None @property def delivery_tag(self) -> int | None: if isinstance( self.delivery, ( spec.Basic.GetOk, spec.Basic.Deliver, ), ): return self.delivery.delivery_tag return None @property def redelivered(self) -> bool | None: if isinstance( self.delivery, ( spec.Basic.GetOk, spec.Basic.Deliver, ), ): return self.delivery.redelivered return None @property def consumer_tag(self) -> str | None: if isinstance(self.delivery, spec.Basic.Deliver): return self.delivery.consumer_tag return None @property def message_count(self) -> int | None: if isinstance(self.delivery, spec.Basic.GetOk): return self.delivery.message_count return None ChannelRType = tuple[int, Channel.OpenOk] CallbackCoro = Coroutine[Any, Any, Any] ConsumerCallback = Callable[[DeliveredMessage], CallbackCoro] ReturnCallback = Callable[[DeliveredMessage], Any] # Called with the Basic.Cancel frame the broker sent for a consumer. ConsumerCancelCallback = Callable[[spec.Basic.Cancel], Any] ArgumentsType = FieldTable ConfirmationFrameType = Basic.Ack | Basic.Nack | Basic.Reject @dataclasses.dataclass(frozen=True) class SSLCerts: cert: str | None key: str | None capath: str | None cafile: str | None cadata: bytes | None verify: bool @dataclasses.dataclass(frozen=True) class FrameReceived: channel: int frame: str URLorStr = URL | str DrainResult = Awaitable[None] TimeoutType = float | int | None FrameType = Frame | ContentHeader | ContentBody RpcReturnType = ( Basic.CancelOk | Basic.ConsumeOk | Basic.GetEmpty | Basic.GetOk | Basic.QosOk | Basic.RecoverOk | Channel.CloseOk | Channel.FlowOk | Channel.OpenOk | Confirm.SelectOk | Exchange.BindOk | Exchange.DeclareOk | Exchange.DeleteOk | Exchange.UnbindOk | Queue.BindOk | Queue.DeclareOk | Queue.DeleteOk | Queue.PurgeOk | Queue.UnbindOk | Tx.CommitOk | Tx.RollbackOk | Tx.SelectOk | None ) @dataclasses.dataclass(frozen=True) class ChannelFrame: payload: bytes should_close: bool drain_future: asyncio.Future | None = None def drain(self) -> None: if not self.should_drain: return if self.drain_future is not None and not self.drain_future.done(): self.drain_future.set_result(None) @property def should_drain(self) -> bool: return self.drain_future is not None and not self.drain_future.done() @classmethod def marshall( cls, channel_number: int, frames: Iterable[FrameType | Heartbeat | ContentBody], drain_future: asyncio.Future | None = None, ) -> "ChannelFrame": should_close = False with io.BytesIO() as fp: for frame in frames: if should_close: logger = logging.getLogger( "aiormq.connection", ).getChild( "marshall", ) logger.warning( "It looks like you are going to send a frame %r after " "the connection is closed, it's pointless, " "the frame is dropped.", frame, ) continue if isinstance(frame, spec.Connection.CloseOk): should_close = True fp.write(pamqp.frame.marshal(frame, channel_number)) return cls( payload=fp.getvalue(), drain_future=drain_future, should_close=should_close, ) class AbstractFutureStore: futures: set[asyncio.Future | TaskType] loop: asyncio.AbstractEventLoop @abstractmethod def add(self, future: asyncio.Future | TaskWrapper) -> None: raise NotImplementedError @abstractmethod def reject_all(self, exception: ExceptionType | None) -> Any: raise NotImplementedError @abstractmethod def create_task(self, coro: CoroutineType) -> TaskType: raise NotImplementedError @abstractmethod def create_future(self) -> asyncio.Future: raise NotImplementedError @abstractmethod def get_child(self) -> "AbstractFutureStore": raise NotImplementedError class AbstractBase(ABC): loop: asyncio.AbstractEventLoop @abstractmethod def _future_store_child(self) -> AbstractFutureStore: raise NotImplementedError @abstractmethod def create_task(self, coro: CoroutineType) -> TaskType: raise NotImplementedError def create_future(self) -> asyncio.Future: raise NotImplementedError @abstractmethod async def _on_close(self, exc: Exception | None = None) -> None: raise NotImplementedError @abstractmethod async def close( self, exc: ExceptionType | None = asyncio.CancelledError(), ) -> None: raise NotImplementedError @abstractmethod def __str__(self) -> str: raise NotImplementedError @property @abstractmethod def is_closed(self) -> bool: raise NotImplementedError class AbstractChannel(AbstractBase): frames: asyncio.Queue connection: "AbstractConnection" number: int on_return_callbacks: set[ReturnCallback] on_consumer_cancel_callbacks: set[ConsumerCancelCallback] @property @abstractmethod def closing(self) -> asyncio.Future: raise NotImplementedError @abstractmethod async def open(self) -> spec.Channel.OpenOk: pass @abstractmethod async def basic_get( self, queue: str = "", no_ack: bool = False, timeout: TimeoutType = None, ) -> DeliveredMessage: raise NotImplementedError @overload async def basic_cancel( self, consumer_tag: str, *, nowait: Literal[False] = False, timeout: TimeoutType = None, ) -> spec.Basic.CancelOk: ... @overload async def basic_cancel( self, consumer_tag: str, *, nowait: Literal[True] = True, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def basic_cancel( self, consumer_tag: str, *, nowait: bool = False, timeout: TimeoutType = None, ) -> spec.Basic.CancelOk | None: raise NotImplementedError @abstractmethod async def basic_consume( self, queue: str, consumer_callback: ConsumerCallback, *, no_ack: bool = False, exclusive: bool = False, arguments: ArgumentsType | None = None, consumer_tag: str | None = None, timeout: TimeoutType = None, ) -> spec.Basic.ConsumeOk: raise NotImplementedError @abstractmethod def basic_ack( self, delivery_tag: int, multiple: bool = False, wait: bool = True, ) -> DrainResult: raise NotImplementedError @abstractmethod def basic_nack( self, delivery_tag: int, multiple: bool = False, requeue: bool = True, wait: bool = True, ) -> DrainResult: raise NotImplementedError @abstractmethod def basic_reject( self, delivery_tag: int, *, requeue: bool = True, wait: bool = True, ) -> DrainResult: raise NotImplementedError @abstractmethod async def basic_publish( self, body: bytes, *, exchange: str = "", routing_key: str = "", properties: spec.Basic.Properties | None = None, mandatory: bool = False, immediate: bool = False, timeout: TimeoutType = None, ) -> ConfirmationFrameType | None: raise NotImplementedError @abstractmethod async def basic_qos( self, *, prefetch_size: int | None = None, prefetch_count: int | None = None, global_: bool = False, timeout: TimeoutType = None, ) -> spec.Basic.QosOk: raise NotImplementedError @overload async def basic_recover( self, *, nowait: Literal[False] = False, requeue: bool = False, timeout: TimeoutType = None, ) -> spec.Basic.RecoverOk: ... @overload async def basic_recover( self, *, nowait: Literal[True] = True, requeue: bool = False, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def basic_recover( self, *, nowait: bool = False, requeue: bool = False, timeout: TimeoutType = None, ) -> spec.Basic.RecoverOk | None: raise NotImplementedError @overload async def exchange_declare( self, exchange: str = "", *, exchange_type: str = "direct", passive: bool = False, durable: bool = False, auto_delete: bool = False, internal: bool = False, nowait: Literal[False] = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.DeclareOk: ... @overload async def exchange_declare( self, exchange: str = "", *, exchange_type: str = "direct", passive: bool = False, durable: bool = False, auto_delete: bool = False, internal: bool = False, nowait: Literal[True] = True, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def exchange_declare( self, exchange: str = "", *, exchange_type: str = "direct", passive: bool = False, durable: bool = False, auto_delete: bool = False, internal: bool = False, nowait: bool = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.DeclareOk | None: raise NotImplementedError @overload async def exchange_delete( self, exchange: str = "", *, if_unused: bool = False, nowait: Literal[False] = False, timeout: TimeoutType = None, ) -> spec.Exchange.DeleteOk: ... @overload async def exchange_delete( self, exchange: str = "", *, if_unused: bool = False, nowait: Literal[True] = True, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def exchange_delete( self, exchange: str = "", *, if_unused: bool = False, nowait: bool = False, timeout: TimeoutType = None, ) -> spec.Exchange.DeleteOk | None: raise NotImplementedError @overload async def exchange_bind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: Literal[False] = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.BindOk: ... @overload async def exchange_bind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: Literal[True] = True, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def exchange_bind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: bool = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.BindOk | None: raise NotImplementedError @overload async def exchange_unbind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: Literal[False] = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.UnbindOk: ... @overload async def exchange_unbind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: Literal[True] = True, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def exchange_unbind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: bool = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.UnbindOk | None: raise NotImplementedError @abstractmethod async def flow( self, active: bool, timeout: TimeoutType = None, ) -> spec.Channel.FlowOk: raise NotImplementedError @overload async def queue_bind( self, queue: str, exchange: str, routing_key: str = "", nowait: Literal[False] = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Queue.BindOk: ... @overload async def queue_bind( self, queue: str, exchange: str, routing_key: str = "", nowait: Literal[True] = True, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def queue_bind( self, queue: str, exchange: str, routing_key: str = "", nowait: bool = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Queue.BindOk | None: raise NotImplementedError @overload async def queue_declare( self, queue: str = "", *, passive: bool = False, durable: bool = False, exclusive: bool = False, auto_delete: bool = False, nowait: Literal[False] = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Queue.DeclareOk: ... @overload async def queue_declare( self, queue: str = "", *, passive: bool = False, durable: bool = False, exclusive: bool = False, auto_delete: bool = False, nowait: Literal[True] = True, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def queue_declare( self, queue: str = "", *, passive: bool = False, durable: bool = False, exclusive: bool = False, auto_delete: bool = False, nowait: bool = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Queue.DeclareOk | None: raise NotImplementedError @overload async def queue_delete( self, queue: str = "", if_unused: bool = False, if_empty: bool = False, nowait: Literal[False] = False, timeout: TimeoutType = None, ) -> spec.Queue.DeleteOk: ... @overload async def queue_delete( self, queue: str = "", if_unused: bool = False, if_empty: bool = False, nowait: Literal[True] = True, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def queue_delete( self, queue: str = "", if_unused: bool = False, if_empty: bool = False, nowait: bool = False, timeout: TimeoutType = None, ) -> spec.Queue.DeleteOk | None: raise NotImplementedError @overload async def queue_purge( self, queue: str = "", nowait: Literal[False] = False, timeout: TimeoutType = None, ) -> spec.Queue.PurgeOk: ... @overload async def queue_purge( self, queue: str = "", nowait: Literal[True] = True, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def queue_purge( self, queue: str = "", nowait: bool = False, timeout: TimeoutType = None, ) -> spec.Queue.PurgeOk | None: raise NotImplementedError @abstractmethod async def queue_unbind( self, queue: str = "", exchange: str = "", routing_key: str = "", arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Queue.UnbindOk: raise NotImplementedError @abstractmethod async def tx_commit( self, timeout: TimeoutType = None, ) -> spec.Tx.CommitOk: raise NotImplementedError @abstractmethod async def tx_rollback( self, timeout: TimeoutType = None, ) -> spec.Tx.RollbackOk: raise NotImplementedError @abstractmethod async def tx_select(self, timeout: TimeoutType = None) -> spec.Tx.SelectOk: raise NotImplementedError @overload async def confirm_delivery( self, nowait: Literal[False] = False, timeout: TimeoutType = None, ) -> spec.Confirm.SelectOk: ... @overload async def confirm_delivery( self, nowait: Literal[True] = True, timeout: TimeoutType = None, ) -> None: ... @abstractmethod async def confirm_delivery( self, nowait: bool = False, timeout: TimeoutType = None, ) -> spec.Confirm.SelectOk | None: raise NotImplementedError class AbstractConnection(AbstractBase): FRAME_BUFFER_SIZE: int = 10 # Interval between sending heartbeats based on the heartbeat(timeout) HEARTBEAT_INTERVAL_MULTIPLIER: TimeoutType # Allow three missed heartbeats (based on heartbeat(timeout) HEARTBEAT_GRACE_MULTIPLIER: int server_properties: ArgumentsType connection_tune: spec.Connection.Tune channels: dict[int, AbstractChannel | None] write_queue: asyncio.Queue url: URL @property @abstractmethod def closing(self) -> asyncio.Future: raise NotImplementedError @abstractmethod def set_close_reason( self, reply_code: int = REPLY_SUCCESS, reply_text: str = "normally closed", class_id: int = 0, method_id: int = 0, ) -> None: raise NotImplementedError @property @abstractmethod def is_opened(self) -> bool: raise NotImplementedError @abstractmethod def __str__(self) -> str: raise NotImplementedError @abstractmethod async def connect( self, client_properties: FieldTable | None = None, ) -> bool: raise NotImplementedError @property @abstractmethod def server_capabilities(self) -> ArgumentsType: raise NotImplementedError @property @abstractmethod def basic_nack(self) -> bool: raise NotImplementedError @property @abstractmethod def consumer_cancel_notify(self) -> bool: raise NotImplementedError @property @abstractmethod def accept_consumer_cancel_ok(self) -> bool: raise NotImplementedError @property @abstractmethod def exchange_exchange_bindings(self) -> bool: raise NotImplementedError @property @abstractmethod def publisher_confirms(self) -> bool | None: raise NotImplementedError async def channel( self, channel_number: int | None = None, publisher_confirms: bool = True, frame_buffer_size: int = FRAME_BUFFER_SIZE, timeout: TimeoutType = None, **kwargs: Any, ) -> AbstractChannel: raise NotImplementedError @abstractmethod async def __aenter__(self) -> "AbstractConnection": raise NotImplementedError @abstractmethod async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: raise NotImplementedError @abstractmethod async def ready(self) -> None: raise NotImplementedError @abstractmethod async def update_secret( self, new_secret: str, *, reason: str = "", timeout: TimeoutType = None, ) -> spec.Connection.UpdateSecretOk: raise NotImplementedError __all__ = ( "AbstractBase", "AbstractChannel", "AbstractConnection", "AbstractFutureStore", "ArgumentsType", "CallbackCoro", "ChannelFrame", "ChannelRType", "ConfirmationFrameType", "ConsumerCallback", "ConsumerCancelCallback", "CoroutineType", "DeliveredMessage", "DrainResult", "ExceptionType", "FieldArray", "FieldTable", "FieldValue", "FrameReceived", "FrameType", "GetResultType", "ReturnCallback", "RpcReturnType", "SSLCerts", "TaskType", "TaskWrapper", "TimeoutType", "URLorStr", ) mosquito-aiormq-612e0d0/aiormq/auth.py000066400000000000000000000014761527161700100200160ustar00rootroot00000000000000import abc from enum import Enum from .abc import AbstractConnection class AuthBase: value: str | None def __init__(self, connector: AbstractConnection): self.connector = connector self.value = None @abc.abstractmethod def encode(self) -> str: raise NotImplementedError def marshal(self) -> str: if self.value is None: self.value = self.encode() return self.value class PlainAuth(AuthBase): def encode(self) -> str: return ( "\x00" + (self.connector.url.user or "guest") + "\x00" + (self.connector.url.password or "guest") ) class ExternalAuth(AuthBase): def encode(self) -> str: return "" class AuthMechanism(Enum): PLAIN = PlainAuth EXTERNAL = ExternalAuth mosquito-aiormq-612e0d0/aiormq/base.py000066400000000000000000000151461527161700100177660ustar00rootroot00000000000000import abc import asyncio from collections.abc import Callable, Coroutine from contextlib import suppress from functools import wraps from typing import Any, Literal, TypeVar from weakref import WeakSet from .abc import ( AbstractBase, AbstractFutureStore, CoroutineType, ExceptionType, TaskType, TaskWrapper, TimeoutType, ) from .tools import Countdown, shield T = TypeVar("T") def _retrieve_exception(future: asyncio.Future) -> None: if not future.cancelled(): future.exception() class FutureStore(AbstractFutureStore): __slots__ = "futures", "loop", "parent" futures: set[asyncio.Future | TaskType] weak_futures: WeakSet loop: asyncio.AbstractEventLoop def __init__(self, loop: asyncio.AbstractEventLoop): self.futures = set() self.loop = loop # False until reject_all() ran. After that every added future is # rejected at once with this reason, so no caller waits forever. self.reject_reason: ExceptionType | Literal[False] | None = False self.parent: FutureStore | None = None def __on_task_done( self, future: asyncio.Future | TaskWrapper, ) -> Callable[..., Any]: def remover(*_: Any) -> None: nonlocal future # noqa if future in self.futures: self.futures.remove(future) return remover def add(self, future: asyncio.Future | TaskWrapper) -> None: if self.reject_reason is not False: if isinstance(future, TaskWrapper): future.throw(self.reject_reason or Exception) elif isinstance(future, asyncio.Future): future.set_exception(self.reject_reason or Exception) self.futures.add(future) future.add_done_callback(self.__on_task_done(future)) if self.parent: self.parent.add(future) @shield async def reject_all(self, exception: ExceptionType | None) -> None: self.reject_reason = exception tasks = [] while self.futures: future: TaskType | asyncio.Future = self.futures.pop() if future.done(): continue if isinstance(future, TaskWrapper): future.throw(exception or Exception) tasks.append(future) elif asyncio.isfuture(future): future.set_exception(exception or Exception) if tasks: await asyncio.gather(*tasks, return_exceptions=True) def create_task(self, coro: CoroutineType) -> TaskType: task: TaskWrapper = TaskWrapper(self.loop.create_task(coro)) self.add(task) return task def create_future(self, weak: bool = False) -> asyncio.Future: future = self.loop.create_future() # A caller can stop waiting for the future before reject_all() # sets its exception, for example a publish that failed on the # drain future while its confirmation was still pending. Read the # exception once, so asyncio does not log "Future exception was # never retrieved" when the future is collected. future.add_done_callback(_retrieve_exception) self.add(future) return future def get_child(self) -> "FutureStore": store = FutureStore(self.loop) store.parent = self return store class Base(AbstractBase): __slots__ = "loop", "__future_store", "_closing" def __init__( self, *, loop: asyncio.AbstractEventLoop, parent: AbstractBase | None = None, ): self.loop: asyncio.AbstractEventLoop = loop if parent: self.__future_store = parent._future_store_child() else: self.__future_store = FutureStore(loop=self.loop) self._closing = self._create_closing_future() def _create_closing_future(self) -> asyncio.Future: return self.__future_store.create_future() @property def closing(self) -> asyncio.Future: """Return an independent observer of closure while the resource is open. Cancelling this future only stops that observer. Use close() to shut down the resource. """ if self._closing.done(): return self._closing future = self.loop.create_future() def on_close(source: asyncio.Future) -> None: if future.done(): return if source.cancelled(): future.cancel() elif (exc := source.exception()) is not None: future.set_exception(exc) else: future.set_result(source.result()) def on_done(observer: asyncio.Future) -> None: self._closing.remove_done_callback(on_close) if not observer.cancelled(): observer.exception() self._closing.add_done_callback(on_close) future.add_done_callback(on_done) return future def _cancel_tasks( self, exc: ExceptionType | None = None, ) -> Coroutine[Any, Any, None]: return self.__future_store.reject_all(exc) def _future_store_child(self) -> AbstractFutureStore: return self.__future_store.get_child() def create_task(self, coro: CoroutineType) -> TaskType: return self.__future_store.create_task(coro) def create_future(self) -> asyncio.Future: return self.__future_store.create_future() @abc.abstractmethod async def _on_close( self, exc: ExceptionType | None = None, ) -> None: # pragma: no cover return async def __closer(self, exc: ExceptionType | None) -> None: if self.is_closed: # pragma: no cover return with suppress(Exception): await self._on_close(exc) with suppress(Exception): await self._cancel_tasks(exc) async def close( self, exc: ExceptionType | None = asyncio.CancelledError, timeout: TimeoutType = None, ) -> None: if self.is_closed: return None countdown = Countdown(timeout) await countdown(self.__closer(exc)) def __repr__(self) -> str: cls_name = self.__class__.__name__ return '<{0}: "{1}" at 0x{2:02x}>'.format( cls_name, str(self), id(self), ) @abc.abstractmethod def __str__(self) -> str: # pragma: no cover raise NotImplementedError @property def is_closed(self) -> bool: return self._closing.done() TaskFunctionType = Callable[..., T] def task(func: TaskFunctionType) -> TaskFunctionType: @wraps(func) async def wrap(self: Base, *args: Any, **kwargs: Any) -> Any: return await self.create_task(func(self, *args, **kwargs)) return wrap mosquito-aiormq-612e0d0/aiormq/channel.py000066400000000000000000001326451527161700100204700ustar00rootroot00000000000000import asyncio import io import logging from collections import OrderedDict from collections.abc import AsyncGenerator, Callable, Mapping from contextlib import asynccontextmanager, suppress from functools import partial, wraps from io import BytesIO from random import getrandbits from types import MappingProxyType from typing import Any, Literal, TypeVar, overload from uuid import UUID import pamqp.frame from pamqp import commands as spec from pamqp.base import Frame from pamqp.body import ContentBody from pamqp.constants import REPLY_SUCCESS from pamqp.header import ContentHeader from aiormq.tools import Countdown, awaitable from .abc import ( AbstractChannel, AbstractConnection, ArgumentsType, ChannelFrame, ConfirmationFrameType, ConsumerCallback, ConsumerCancelCallback, DeliveredMessage, ExceptionType, FrameType, GetResultType, ReturnCallback, RpcReturnType, TimeoutType, ) from .base import Base from .exceptions import ( AMQPChannelError, AMQPError, ChannelAccessRefused, ChannelClosed, ChannelInvalidStateError, ChannelLockedResource, ChannelNotFoundEntity, ChannelPreconditionFailed, DeliveryError, DuplicateConsumerTag, InvalidFrameError, MethodNotImplemented, PublishError, ) log = logging.getLogger(__name__) EXCEPTION_MAPPING: Mapping[int, type[AMQPChannelError]] = MappingProxyType({ 403: ChannelAccessRefused, 404: ChannelNotFoundEntity, 405: ChannelLockedResource, 406: ChannelPreconditionFailed, }) T = TypeVar("T") TaskFunctionType = Callable[..., T] def task(func: TaskFunctionType) -> TaskFunctionType: """Run the method as a channel task. Reject the call on a closed channel. A call that starts after the channel closed would create a future in a rejected FutureStore and wait for it forever. """ @wraps(func) async def wrap(self: "Channel", *args: Any, **kwargs: Any) -> Any: if self.is_closed: raise ChannelInvalidStateError("%r closed" % self) return await self.create_task(func(self, *args, **kwargs)) return wrap def exception_by_code(frame: spec.Channel.Close) -> AMQPError: if frame.reply_code is None: return ChannelClosed(frame.reply_code, frame.reply_text) exception_class = EXCEPTION_MAPPING.get(frame.reply_code) if exception_class is None: return ChannelClosed(frame.reply_code, frame.reply_text) return exception_class(frame.reply_text) def _check_routing_key(key: str) -> None: if len(key) > 255: raise ValueError("Routing key too long (max 255 bytes)") class Returning(asyncio.Future): pass ConfirmationType = asyncio.Future | Returning class Channel(Base, AbstractChannel): # noinspection PyTypeChecker CONTENT_FRAME_SIZE = len(pamqp.frame.marshal(ContentBody(b""), 0)) CHANNEL_CLOSE_TIMEOUT = 10 confirmations: dict[int, ConfirmationType] def __init__( self, connector: AbstractConnection, number: int, publisher_confirms: bool = True, frame_buffer: int | None = None, on_return_raises: bool = True, ): super().__init__(loop=connector.loop, parent=connector) self.connection = connector if ( publisher_confirms and not connector.publisher_confirms ): # pragma: no cover raise ValueError("Server doesn't support publisher confirms") self.consumers: dict[str, ConsumerCallback] = {} self.confirmations = OrderedDict() self.message_id_delivery_tag: dict[str, int] = dict() self.delivery_tag = 0 self.getter: asyncio.Future | None = None self.getter_lock = asyncio.Lock() self.frames: asyncio.Queue = asyncio.Queue(maxsize=frame_buffer or 0) self.max_content_size = ( connector.connection_tune.frame_max - self.CONTENT_FRAME_SIZE ) self.__lock = asyncio.Lock() self.number: int = number self.publisher_confirms = publisher_confirms self.rpc_frames: asyncio.Queue = asyncio.Queue( maxsize=frame_buffer or 0, ) self.write_queue = connector.write_queue self.on_return_raises = on_return_raises self.on_return_callbacks: set[ReturnCallback] = set() self.on_consumer_cancel_callbacks: set[ ConsumerCancelCallback ] = set() self._close_exception: BaseException | None = None self.__reader_task = self.create_task(self._reader()) self.__close_reply_code: int = REPLY_SUCCESS self.__close_reply_text: str = "" self.__close_class_id: int = 0 self.__close_method_id: int = 0 self.__close_event: asyncio.Event = asyncio.Event() # True after Channel.Open reached the writer. From that point # the broker has (or will have) a channel with this number. self.__open_sent = False def set_close_reason( self, reply_code: int = REPLY_SUCCESS, reply_text: str = "", class_id: int = 0, method_id: int = 0, ) -> None: self.__close_reply_code = reply_code self.__close_reply_text = reply_text self.__close_class_id = class_id self.__close_method_id = method_id @property @asynccontextmanager async def lock(self) -> AsyncGenerator[None, None]: """Hold the channel lock. Reject the caller if the channel closed. The second check covers a close that happened while the caller waited for the lock. """ if self.is_closed: raise ChannelInvalidStateError("%r closed" % self) async with self.__lock: if self.is_closed: raise ChannelInvalidStateError("%r closed" % self) yield async def _get_frame(self) -> FrameType: weight, frame = await self.frames.get() self.frames.task_done() return frame def __str__(self) -> str: return str(self.number) # The overloads give each RPC call the type of its reply frame. # A call with nowait=True returns None; see the last overload. @overload async def rpc( self, frame: spec.Channel.Open, timeout: TimeoutType = None, ) -> spec.Channel.OpenOk: ... @overload async def rpc( self, frame: spec.Channel.Close, timeout: TimeoutType = None, ) -> spec.Channel.CloseOk: ... @overload async def rpc( self, frame: spec.Channel.Flow, timeout: TimeoutType = None, ) -> spec.Channel.FlowOk: ... @overload async def rpc( self, frame: spec.Confirm.Select, timeout: TimeoutType = None, ) -> spec.Confirm.SelectOk: ... @overload async def rpc( self, frame: spec.Basic.Get, timeout: TimeoutType = None, ) -> GetResultType: ... @overload async def rpc( self, frame: spec.Basic.Cancel, timeout: TimeoutType = None, ) -> spec.Basic.CancelOk: ... @overload async def rpc( self, frame: spec.Basic.Consume, timeout: TimeoutType = None, ) -> spec.Basic.ConsumeOk: ... @overload async def rpc( self, frame: spec.Basic.Qos, timeout: TimeoutType = None, ) -> spec.Basic.QosOk: ... @overload async def rpc( self, frame: spec.Basic.Recover | spec.Basic.RecoverAsync, timeout: TimeoutType = None, ) -> spec.Basic.RecoverOk: ... @overload async def rpc( self, frame: spec.Exchange.Declare, timeout: TimeoutType = None, ) -> spec.Exchange.DeclareOk: ... @overload async def rpc( self, frame: spec.Exchange.Delete, timeout: TimeoutType = None, ) -> spec.Exchange.DeleteOk: ... @overload async def rpc( self, frame: spec.Exchange.Bind, timeout: TimeoutType = None, ) -> spec.Exchange.BindOk: ... @overload async def rpc( self, frame: spec.Exchange.Unbind, timeout: TimeoutType = None, ) -> spec.Exchange.UnbindOk: ... @overload async def rpc( self, frame: spec.Queue.Declare, timeout: TimeoutType = None, ) -> spec.Queue.DeclareOk: ... @overload async def rpc( self, frame: spec.Queue.Delete, timeout: TimeoutType = None, ) -> spec.Queue.DeleteOk: ... @overload async def rpc( self, frame: spec.Queue.Bind, timeout: TimeoutType = None, ) -> spec.Queue.BindOk: ... @overload async def rpc( self, frame: spec.Queue.Unbind, timeout: TimeoutType = None, ) -> spec.Queue.UnbindOk: ... @overload async def rpc( self, frame: spec.Queue.Purge, timeout: TimeoutType = None, ) -> spec.Queue.PurgeOk: ... @overload async def rpc( self, frame: spec.Tx.Select, timeout: TimeoutType = None, ) -> spec.Tx.SelectOk: ... @overload async def rpc( self, frame: spec.Tx.Commit, timeout: TimeoutType = None, ) -> spec.Tx.CommitOk: ... @overload async def rpc( self, frame: spec.Tx.Rollback, timeout: TimeoutType = None, ) -> spec.Tx.RollbackOk: ... @overload async def rpc( self, frame: Frame, timeout: TimeoutType = None, ) -> RpcReturnType: ... @task async def rpc( self, frame: Frame, timeout: TimeoutType = None, ) -> RpcReturnType: if self.__close_event.is_set(): raise ChannelInvalidStateError("Channel closed by RPC timeout") countdown = Countdown(timeout) lock = self.lock async with countdown.enter_context(lock): # Another call can close the channel while this call waits for # the lock. Do not send a frame on a closed channel. if self.__close_event.is_set(): raise ChannelInvalidStateError("Channel closed by RPC timeout") try: await countdown( self.write_queue.put( ChannelFrame.marshall( channel_number=self.number, frames=[frame], ), ), ) if isinstance(frame, spec.Channel.Open): self.__open_sent = True # An asynchronous method has no reply. A synchronous method # sent with nowait gets no reply either, because AMQP 0-9-1 # forbids the broker to answer a no-wait request. if not frame.synchronous or getattr(frame, "nowait", False): return None result = await countdown(self.rpc_frames.get()) self.rpc_frames.task_done() if result.name not in frame.valid_responses: # pragma: no cover raise InvalidFrameError(frame) return result except (asyncio.CancelledError, asyncio.TimeoutError): if self.is_closed: raise await self.__close_after_failure(frame) raise async def __close_after_failure(self, frame: Frame | None) -> None: """Close the channel after a cancelled or timed out RPC call. When Channel.Open reached the writer, the broker has a channel with this number and a Channel.Close handshake frees the number. Otherwise the channel is closed locally. """ self.__close_event.set() if not self.__open_sent: log.warning( "Closing channel %r locally because RPC call %s cancelled " "before Channel.Open was sent", self, frame, ) self._close_locally() return log.warning( "Closing channel %r because RPC call %s cancelled", self, frame, ) await self.write_queue.put( ChannelFrame.marshall( channel_number=self.number, frames=[ spec.Channel.Close( class_id=0, method_id=0, reply_code=504, reply_text="RPC timeout on frame {!s}".format(frame), ), ], ), ) def _close_locally(self) -> None: """Close the channel without a Channel.Close handshake. Use it only when Channel.Open did not reach the writer. The broker has no channel with this number, so the number is free at once. """ self.__close_event.set() self.__reader_task.cancel() if self.connection.channels.get(self.number) is self: del self.connection.channels[self.number] async def open(self, timeout: TimeoutType = None) -> spec.Channel.OpenOk: try: frame: spec.Channel.OpenOk = await self.rpc( spec.Channel.Open(), timeout=timeout, ) if self.publisher_confirms: await self.rpc(spec.Confirm.Select()) except BaseException: if not self.__close_event.is_set(): # rpc() did not run, or it failed before its own cleanup. # Cancellation before the rpc task starts takes this path, # also between Channel.OpenOk and Confirm.Select. await self.__close_after_failure(None) raise if frame is None: # pragma: no cover raise InvalidFrameError(frame) return frame async def __get_content_frame(self) -> ContentBody: content_frame = await self._get_frame() if not isinstance(content_frame, ContentBody): raise ValueError( "Unexpected frame {!r}".format(content_frame), content_frame, ) return content_frame async def _read_content( self, frame: spec.Basic.Deliver | spec.Basic.Return | GetResultType, header: ContentHeader, ) -> DeliveredMessage: with BytesIO() as body: content: ContentBody | None = None if header.body_size: content = await self.__get_content_frame() while content and body.tell() < header.body_size: body.write(content.value) if body.tell() < header.body_size: content = await self.__get_content_frame() return DeliveredMessage( delivery=frame, header=header, body=body.getvalue(), channel=self, ) async def __get_content_header(self) -> ContentHeader: frame: FrameType = await self._get_frame() if not isinstance(frame, ContentHeader): raise ValueError( "Unexpected frame {!r} instead of ContentHeader".format(frame), frame, ) return frame @staticmethod def _on_consumer_done(task: asyncio.Task) -> None: if task.cancelled(): return exception = task.exception() if exception is not None: log.error("Consumer callback failed", exc_info=exception) async def _on_deliver_frame(self, frame: spec.Basic.Deliver) -> None: header: ContentHeader = await self.__get_content_header() message = await self._read_content(frame, header) if frame.consumer_tag is None: log.warning("Frame %r has no consumer tag", frame) return consumer = self.consumers.get(frame.consumer_tag) if consumer is not None: # noinspection PyAsyncCall task = self.create_task(consumer(message)) task.add_done_callback(self._on_consumer_done) async def _on_get_frame( self, frame: spec.Basic.GetOk | spec.Basic.GetEmpty, ) -> None: message = None if isinstance(frame, spec.Basic.GetOk): header: ContentHeader = await self.__get_content_header() message = await self._read_content(frame, header) if isinstance(frame, spec.Basic.GetEmpty): message = DeliveredMessage( delivery=frame, header=ContentHeader(), body=b"", channel=self, ) getter = getattr(self, "getter", None) if getter is None: raise RuntimeError("Getter is None") if getter.done(): log.error("Got message but no active getter") return getter.set_result((frame, message)) return async def _on_return_frame(self, frame: spec.Basic.Return) -> None: header: ContentHeader = await self.__get_content_header() message = await self._read_content(frame, header) message_id = message.header.properties.message_id if message_id is None: log.error("message_id if None on returned message %r", message) return delivery_tag = self.message_id_delivery_tag.get(message_id) if delivery_tag is None: # pragma: nocover log.error("Unhandled message %r returning", message) return confirmation = self.confirmations.pop(delivery_tag, None) if confirmation is None: # pragma: nocover return self.confirmations[delivery_tag] = Returning() if self.on_return_raises: confirmation.set_exception(PublishError(message, frame)) return for cb in self.on_return_callbacks: # noinspection PyBroadException try: cb(message) except Exception: log.exception("Unhandled return callback exception") confirmation.set_result(message) def _confirm_delivery( self, delivery_tag: int | None, frame: ConfirmationFrameType, ) -> None: if delivery_tag not in self.confirmations: return confirmation = self.confirmations.pop(delivery_tag) if isinstance(confirmation, Returning): return elif confirmation.done(): # Cancellation or a publish timeout can precede the broker's reply. level = ( logging.DEBUG if confirmation.cancelled() else logging.WARNING ) log.log( level, "Delivery tag %r confirmed %r was ignored", delivery_tag, frame, ) return elif isinstance(frame, spec.Basic.Ack): confirmation.set_result(frame) return confirmation.set_exception( DeliveryError(None, frame), ) # pragma: nocover async def _on_confirm_frame(self, frame: ConfirmationFrameType) -> None: if not self.publisher_confirms: # pragma: nocover return if frame.delivery_tag not in self.confirmations: log.error("Unexpected confirmation frame %r from broker", frame) return multiple = getattr(frame, "multiple", False) if multiple: for delivery_tag in self.confirmations.keys(): if frame.delivery_tag >= delivery_tag: # Should be called later to avoid keys copying self.loop.call_soon( self._confirm_delivery, delivery_tag, frame, ) else: self._confirm_delivery(frame.delivery_tag, frame) async def _on_cancel_frame( self, frame: spec.Basic.CancelOk | spec.Basic.Cancel, ) -> None: was_consuming = frame.consumer_tag in self.consumers if frame.consumer_tag is not None: self.consumers.pop(frame.consumer_tag, None) if not isinstance(frame, spec.Basic.Cancel): # Basic.CancelOk answers a client basic_cancel() call. return if was_consuming and self.connection.accept_consumer_cancel_ok: # RabbitMQ 4.3 and later keeps the consumer tag until the client # answers the broker's Basic.Cancel. The broker always sets the # nowait flag in that frame, so only the server capability # decides. Older brokers close the channel on an unexpected # Basic.CancelOk, therefore the answer stays capability-gated. # The queue is a FIFO shared with the connection writer, so a # callback that consumes again with the same tag reaches the # broker after this answer. await self.write_queue.put( ChannelFrame.marshall( channel_number=self.number, frames=[ spec.Basic.CancelOk(consumer_tag=frame.consumer_tag), ], ), ) # The broker cancelled the consumer, for example because the queue # was deleted. Tell the user, a callback error must not stop the # channel reader. log.warning( "Consumer %r cancelled by the broker on %r", frame.consumer_tag, self, ) for cb in self.on_consumer_cancel_callbacks: # noinspection PyBroadException try: await awaitable(cb)(frame) except Exception: log.exception("Unhandled consumer cancel callback exception") async def _on_close_frame(self, frame: spec.Channel.Close) -> None: exc: BaseException = exception_by_code(frame) self._close_exception = exc with suppress(asyncio.QueueFull): self.write_queue.put_nowait( ChannelFrame.marshall( channel_number=self.number, frames=[spec.Channel.CloseOk()], ), ) self.__close_event.set() raise exc async def _on_close_ok_frame(self, _: spec.Channel.CloseOk) -> None: self.__close_event.set() raise ChannelClosed(None, None) async def _reader(self) -> None: last_exception: BaseException | None = None try: while True: frame = await self._get_frame() should_add_to_rpc = False match frame: case spec.Basic.Deliver(): await self._on_deliver_frame(frame) case spec.Basic.GetOk() | spec.Basic.GetEmpty(): await self._on_get_frame(frame) should_add_to_rpc = True case spec.Basic.Return(): await self._on_return_frame(frame) case spec.Basic.Cancel(): await self._on_cancel_frame(frame) case spec.Basic.CancelOk(): await self._on_cancel_frame(frame) should_add_to_rpc = True case spec.Channel.Close(): await self._on_close_frame(frame) case spec.Channel.CloseOk(): await self._on_close_ok_frame(frame) case spec.Basic.Ack() | spec.Basic.Nack(): await self._on_confirm_frame(frame) case _: should_add_to_rpc = True if should_add_to_rpc: await self.rpc_frames.put(frame) except asyncio.CancelledError as e: self.__close_event.set() last_exception = e return except Exception as e: last_exception = e raise finally: await self.close( last_exception, timeout=self.CHANNEL_CLOSE_TIMEOUT, ) @task async def _on_close(self, exc: ExceptionType | None = None) -> None: if not self.connection.is_opened or self.__close_event.is_set(): return await self.rpc( spec.Channel.Close( reply_code=self.__close_reply_code, class_id=self.__close_class_id, method_id=self.__close_method_id, ), timeout=self.connection.connection_tune.heartbeat or None, ) await self.__close_event.wait() async def basic_get( self, queue: str = "", no_ack: bool = False, timeout: TimeoutType = None, ) -> DeliveredMessage: countdown = Countdown(timeout) async with countdown.enter_context(self.getter_lock): self.getter = self.create_future() try: await self.rpc( spec.Basic.Get(queue=queue, no_ack=no_ack), timeout=countdown.get_timeout(), ) except BaseException: self.getter.cancel() raise else: frame: spec.Basic.GetEmpty | spec.Basic.GetOk message: DeliveredMessage frame, message = await countdown(self.getter) finally: del self.getter return message @overload async def basic_cancel( self, consumer_tag: str, *, nowait: Literal[False] = False, timeout: TimeoutType = None, ) -> spec.Basic.CancelOk: ... @overload async def basic_cancel( self, consumer_tag: str, *, nowait: Literal[True] = True, timeout: TimeoutType = None, ) -> None: ... async def basic_cancel( self, consumer_tag: str, *, nowait: bool = False, timeout: TimeoutType = None, ) -> spec.Basic.CancelOk | None: result = await self.rpc( spec.Basic.Cancel(consumer_tag=consumer_tag, nowait=nowait), timeout=timeout, ) if nowait: # No Basic.CancelOk arrives, so remove the consumer here. self.consumers.pop(consumer_tag, None) return result async def basic_consume( self, queue: str, consumer_callback: ConsumerCallback, *, no_ack: bool = False, exclusive: bool = False, arguments: ArgumentsType | None = None, consumer_tag: str | None = None, timeout: TimeoutType = None, ) -> spec.Basic.ConsumeOk: consumer_tag = consumer_tag or "ctag%i.%s" % ( self.number, UUID(int=getrandbits(128), version=4).hex, ) if consumer_tag in self.consumers: raise DuplicateConsumerTag(self.number) self.consumers[consumer_tag] = awaitable(consumer_callback) return await self.rpc( spec.Basic.Consume( queue=queue, no_ack=no_ack, exclusive=exclusive, consumer_tag=consumer_tag, arguments=arguments, ), timeout=timeout, ) async def __send_and_drain(self, frame: Frame, *, wait: bool) -> None: drain_future = self.create_future() if wait else None try: await self.write_queue.put( ChannelFrame.marshall( frames=[frame], channel_number=self.number, drain_future=drain_future, ), ) except BaseException: if drain_future is not None: drain_future.cancel() raise if drain_future is not None: await drain_future async def basic_ack( self, delivery_tag: int, multiple: bool = False, wait: bool = True, ) -> None: if self.is_closed or self.__close_event.is_set(): raise ChannelInvalidStateError("%r closed" % self) await self.__send_and_drain( spec.Basic.Ack(delivery_tag=delivery_tag, multiple=multiple), wait=wait, ) async def basic_nack( self, delivery_tag: int, multiple: bool = False, requeue: bool = True, wait: bool = True, ) -> None: if self.is_closed or self.__close_event.is_set(): raise ChannelInvalidStateError("%r closed" % self) if not self.connection.basic_nack: raise MethodNotImplemented await self.__send_and_drain( spec.Basic.Nack( delivery_tag=delivery_tag, multiple=multiple, requeue=requeue, ), wait=wait, ) async def basic_reject( self, delivery_tag: int, *, requeue: bool = True, wait: bool = True, ) -> None: if self.is_closed or self.__close_event.is_set(): raise ChannelInvalidStateError("%r closed" % self) await self.__send_and_drain( spec.Basic.Reject(delivery_tag=delivery_tag, requeue=requeue), wait=wait, ) def _split_body(self, body: bytes) -> list[ContentBody]: if not body: return [] if len(body) < self.max_content_size: return [ContentBody(body)] with io.BytesIO(body) as fp: reader = partial(fp.read, self.max_content_size) return list(map(ContentBody, iter(reader, b""))) @asynccontextmanager async def _raise_on_channel_close(self) -> AsyncGenerator[None, None]: try: yield except asyncio.CancelledError as exc: # Closing the channel cancels its consumer tasks as well as # rejecting pending confirmations. Inside a consumer, expose # the error that caused this cancellation to the publisher. task = asyncio.current_task() if self._closing.done() and not self._closing.cancelled(): reason = self._close_exception if ( isinstance(reason, Exception) and self._closing.exception() is reason and len(exc.args) == 1 and exc.args[0] is reason and task is not None and task.cancelling() == 1 ): # Remove only our cancellation. A concurrent external # cancellation must remain a CancelledError. task.uncancel() raise reason from exc raise async def basic_publish( self, body: bytes, *, exchange: str = "", routing_key: str = "", properties: spec.Basic.Properties | None = None, mandatory: bool = False, immediate: bool = False, timeout: TimeoutType = None, wait: bool = True, ) -> ConfirmationFrameType | None: _check_routing_key(routing_key) countdown = Countdown(timeout=timeout) publish_frame = spec.Basic.Publish( exchange=exchange, routing_key=routing_key, mandatory=mandatory, immediate=immediate, ) content_header = ContentHeader( properties=properties or spec.Basic.Properties(delivery_mode=1), body_size=len(body), ) if not content_header.properties.message_id: # UUID compatible random bytes rnd_uuid = UUID(int=getrandbits(128), version=4) content_header.properties.message_id = rnd_uuid.hex confirmation: asyncio.Future | None = None # Keep cancellation in this task: a wait_for task could enqueue a # frame just before its caller is cancelled, making rollback unsafe. async with ( self._raise_on_channel_close(), asyncio.timeout(countdown.get_timeout()), ): async with self.lock: self.delivery_tag += 1 delivery_tag = self.delivery_tag message_id = content_header.properties.message_id drain_future: asyncio.Future | None = None sent = False try: if self.publisher_confirms: confirmation = self.create_future() self.confirmations[delivery_tag] = confirmation self.message_id_delivery_tag[message_id] = delivery_tag def forget_message_id(_: asyncio.Future) -> None: if self.message_id_delivery_tag.get( message_id, ) == delivery_tag: self.message_id_delivery_tag.pop(message_id) body_frames: list[FrameType | ContentBody] body_frames = [publish_frame, content_header] body_frames += self._split_body(body) drain_future = self.create_future() if wait else None await self.write_queue.put( ChannelFrame.marshall( frames=body_frames, channel_number=self.number, drain_future=drain_future, ), ) sent = True if confirmation is not None: confirmation.add_done_callback(forget_message_id) if drain_future is not None: await drain_future except BaseException: if not sent: # The broker has not seen this publish. Reuse its # sequence number while still holding the lock. self.delivery_tag -= 1 self.confirmations.pop(delivery_tag, None) if self.message_id_delivery_tag.get( message_id, ) == delivery_tag: self.message_id_delivery_tag.pop(message_id) if confirmation is not None: confirmation.cancel() if drain_future is not None: drain_future.cancel() raise if confirmation is None: return None return await confirmation async def basic_qos( self, *, prefetch_size: int | None = None, prefetch_count: int | None = None, global_: bool = False, timeout: TimeoutType = None, ) -> spec.Basic.QosOk: return await self.rpc( spec.Basic.Qos( prefetch_size=prefetch_size or 0, prefetch_count=prefetch_count or 0, global_=global_, ), timeout=timeout, ) @overload async def basic_recover( self, *, nowait: Literal[False] = False, requeue: bool = False, timeout: TimeoutType = None, ) -> spec.Basic.RecoverOk: ... @overload async def basic_recover( self, *, nowait: Literal[True] = True, requeue: bool = False, timeout: TimeoutType = None, ) -> None: ... async def basic_recover( self, *, nowait: bool = False, requeue: bool = False, timeout: TimeoutType = None, ) -> spec.Basic.RecoverOk | None: frame: spec.Basic.RecoverAsync | spec.Basic.Recover if nowait: frame = spec.Basic.RecoverAsync(requeue=requeue) else: frame = spec.Basic.Recover(requeue=requeue) return await self.rpc(frame, timeout=timeout) @overload async def exchange_declare( self, exchange: str = "", *, exchange_type: str = "direct", passive: bool = False, durable: bool = False, auto_delete: bool = False, internal: bool = False, nowait: Literal[False] = False, arguments: dict[str, Any] | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.DeclareOk: ... @overload async def exchange_declare( self, exchange: str = "", *, exchange_type: str = "direct", passive: bool = False, durable: bool = False, auto_delete: bool = False, internal: bool = False, nowait: Literal[True] = True, arguments: dict[str, Any] | None = None, timeout: TimeoutType = None, ) -> None: ... async def exchange_declare( self, exchange: str = "", *, exchange_type: str = "direct", passive: bool = False, durable: bool = False, auto_delete: bool = False, internal: bool = False, nowait: bool = False, arguments: dict[str, Any] | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.DeclareOk | None: return await self.rpc( spec.Exchange.Declare( exchange=str(exchange), exchange_type=str(exchange_type), passive=bool(passive), durable=bool(durable), auto_delete=bool(auto_delete), internal=bool(internal), nowait=bool(nowait), arguments=arguments, ), timeout=timeout, ) @overload async def exchange_delete( self, exchange: str = "", *, if_unused: bool = False, nowait: Literal[False] = False, timeout: TimeoutType = None, ) -> spec.Exchange.DeleteOk: ... @overload async def exchange_delete( self, exchange: str = "", *, if_unused: bool = False, nowait: Literal[True] = True, timeout: TimeoutType = None, ) -> None: ... async def exchange_delete( self, exchange: str = "", *, if_unused: bool = False, nowait: bool = False, timeout: TimeoutType = None, ) -> spec.Exchange.DeleteOk | None: return await self.rpc( spec.Exchange.Delete( exchange=exchange, nowait=nowait, if_unused=if_unused, ), timeout=timeout, ) @overload async def exchange_bind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: Literal[False] = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.BindOk: ... @overload async def exchange_bind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: Literal[True] = True, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> None: ... async def exchange_bind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: bool = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.BindOk | None: _check_routing_key(routing_key) return await self.rpc( spec.Exchange.Bind( destination=destination, source=source, routing_key=routing_key, nowait=nowait, arguments=arguments, ), timeout=timeout, ) @overload async def exchange_unbind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: Literal[False] = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.UnbindOk: ... @overload async def exchange_unbind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: Literal[True] = True, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> None: ... async def exchange_unbind( self, destination: str = "", source: str = "", routing_key: str = "", *, nowait: bool = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Exchange.UnbindOk | None: _check_routing_key(routing_key) return await self.rpc( spec.Exchange.Unbind( destination=destination, source=source, routing_key=routing_key, nowait=nowait, arguments=arguments, ), timeout=timeout, ) async def flow( self, active: bool, timeout: TimeoutType = None, ) -> spec.Channel.FlowOk: return await self.rpc( spec.Channel.Flow(active=active), timeout=timeout, ) @overload async def queue_bind( self, queue: str, exchange: str, routing_key: str = "", nowait: Literal[False] = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Queue.BindOk: ... @overload async def queue_bind( self, queue: str, exchange: str, routing_key: str = "", nowait: Literal[True] = True, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> None: ... async def queue_bind( self, queue: str, exchange: str, routing_key: str = "", nowait: bool = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Queue.BindOk | None: _check_routing_key(routing_key) return await self.rpc( spec.Queue.Bind( queue=queue, exchange=exchange, routing_key=routing_key, nowait=nowait, arguments=arguments, ), timeout=timeout, ) @overload async def queue_declare( self, queue: str = "", *, passive: bool = False, durable: bool = False, exclusive: bool = False, auto_delete: bool = False, nowait: Literal[False] = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Queue.DeclareOk: ... @overload async def queue_declare( self, queue: str = "", *, passive: bool = False, durable: bool = False, exclusive: bool = False, auto_delete: bool = False, nowait: Literal[True] = True, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> None: ... async def queue_declare( self, queue: str = "", *, passive: bool = False, durable: bool = False, exclusive: bool = False, auto_delete: bool = False, nowait: bool = False, arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Queue.DeclareOk | None: return await self.rpc( spec.Queue.Declare( queue=queue, passive=bool(passive), durable=bool(durable), exclusive=bool(exclusive), auto_delete=bool(auto_delete), nowait=bool(nowait), arguments=arguments, ), timeout=timeout, ) @overload async def queue_delete( self, queue: str = "", if_unused: bool = False, if_empty: bool = False, nowait: Literal[False] = False, timeout: TimeoutType = None, ) -> spec.Queue.DeleteOk: ... @overload async def queue_delete( self, queue: str = "", if_unused: bool = False, if_empty: bool = False, nowait: Literal[True] = True, timeout: TimeoutType = None, ) -> None: ... async def queue_delete( self, queue: str = "", if_unused: bool = False, if_empty: bool = False, nowait: bool = False, timeout: TimeoutType = None, ) -> spec.Queue.DeleteOk | None: return await self.rpc( spec.Queue.Delete( queue=queue, if_unused=if_unused, if_empty=if_empty, nowait=nowait, ), timeout=timeout, ) @overload async def queue_purge( self, queue: str = "", nowait: Literal[False] = False, timeout: TimeoutType = None, ) -> spec.Queue.PurgeOk: ... @overload async def queue_purge( self, queue: str = "", nowait: Literal[True] = True, timeout: TimeoutType = None, ) -> None: ... async def queue_purge( self, queue: str = "", nowait: bool = False, timeout: TimeoutType = None, ) -> spec.Queue.PurgeOk | None: return await self.rpc( spec.Queue.Purge(queue=queue, nowait=nowait), timeout=timeout, ) async def queue_unbind( self, queue: str = "", exchange: str = "", routing_key: str = "", arguments: ArgumentsType | None = None, timeout: TimeoutType = None, ) -> spec.Queue.UnbindOk: _check_routing_key(routing_key) return await self.rpc( spec.Queue.Unbind( routing_key=routing_key, arguments=arguments, queue=queue, exchange=exchange, ), timeout=timeout, ) async def tx_commit( self, timeout: TimeoutType = None, ) -> spec.Tx.CommitOk: return await self.rpc(spec.Tx.Commit(), timeout=timeout) async def tx_rollback( self, timeout: TimeoutType = None, ) -> spec.Tx.RollbackOk: return await self.rpc(spec.Tx.Rollback(), timeout=timeout) async def tx_select(self, timeout: TimeoutType = None) -> spec.Tx.SelectOk: return await self.rpc(spec.Tx.Select(), timeout=timeout) @overload async def confirm_delivery( self, nowait: Literal[False] = False, timeout: TimeoutType = None, ) -> spec.Confirm.SelectOk: ... @overload async def confirm_delivery( self, nowait: Literal[True] = True, timeout: TimeoutType = None, ) -> None: ... async def confirm_delivery( self, nowait: bool = False, timeout: TimeoutType = None, ) -> spec.Confirm.SelectOk | None: return await self.rpc( spec.Confirm.Select(nowait=nowait), timeout=timeout, ) mosquito-aiormq-612e0d0/aiormq/connection.py000066400000000000000000001140041527161700100212040ustar00rootroot00000000000000import asyncio import inspect import logging import platform import ssl import sys from abc import abstractmethod, ABC from base64 import b64decode from collections.abc import ( AsyncIterable, Coroutine, Generator, Mapping, ) from contextlib import suppress from importlib.metadata import version from io import BytesIO from types import MappingProxyType, TracebackType from typing import Any import pamqp.frame from pamqp import commands as spec from pamqp.base import Frame from pamqp.common import FieldTable from pamqp.constants import REPLY_SUCCESS from pamqp.exceptions import PAMQPException from pamqp.frame import FrameTypes from pamqp.header import ProtocolHeader from pamqp.heartbeat import Heartbeat from yarl import URL from .abc import ( AbstractChannel, AbstractConnection, ArgumentsType, ChannelFrame, ExceptionType, SSLCerts, TaskType, URLorStr, ) from .auth import AuthMechanism from .base import Base, task from .channel import Channel from .exceptions import ( AMQPConnectionError, AMQPError, AuthenticationError, ConnectionChannelError, ConnectionClosed, ConnectionCommandInvalid, ConnectionFrameError, ConnectionInternalError, ConnectionNotAllowed, ConnectionNotImplemented, ConnectionResourceError, ConnectionSyntaxError, ConnectionUnexpectedFrame, IncompatibleProtocolError, InvalidFrameError, ProbableAuthenticationError, ProtocolSyntaxError, ) from .tools import Countdown, censor_url __version__ = version("aiormq") log = logging.getLogger(__name__) CHANNEL_CLOSE_RESPONSES = (spec.Channel.Close, spec.Channel.CloseOk) DEFAULT_PORTS = { "amqp": 5672, "amqps": 5671, } PRODUCT = "aiormq" PLATFORM = "{} {} ({} build {})".format( platform.python_implementation(), platform.python_version(), *platform.python_build(), ) TimeType = float | int TimeoutType = TimeType | None ReceivedFrame = tuple[int, int, FrameTypes] EXCEPTION_MAPPING = MappingProxyType({ 501: ConnectionFrameError, 502: ConnectionSyntaxError, 503: ConnectionCommandInvalid, 504: ConnectionChannelError, 505: ConnectionUnexpectedFrame, 506: ConnectionResourceError, 530: ConnectionNotAllowed, 540: ConnectionNotImplemented, 541: ConnectionInternalError, }) def exception_by_code(frame: spec.Connection.Close) -> AMQPError: if frame.reply_code is None: return ConnectionClosed(frame.reply_code, frame.reply_text) exc_class = EXCEPTION_MAPPING.get(frame.reply_code) if exc_class is None: return ConnectionClosed(frame.reply_code, frame.reply_text) return exc_class(frame.reply_text) def parse_bool(v: Any) -> bool: if isinstance(v, bool): return v v = str(v) return v.lower() in ( "true", "yes", "y", "enable", "on", "enabled", "1" ) def parse_int(v: Any) -> int: if isinstance(v, int): return v v = str(v) try: return int(v) except ValueError: return 0 def parse_timeout(v: Any) -> TimeoutType: if isinstance(v, float): if v.is_integer(): return int(v) return v if isinstance(v, int): return v v = str(v) try: if "." in v: result = float(v) if result.is_integer(): return int(result) return result return int(v) except ValueError: return 0 def parse_heartbeat(v: str) -> int: result = parse_int(v) return result if 0 <= result < 65535 else 0 def parse_connection_name(connection_name: str | None) -> dict[str, str]: if not connection_name or not isinstance(connection_name, str): return {} return dict(connection_name=connection_name) class FrameReceiver(AsyncIterable): _loop: asyncio.AbstractEventLoop def __init__( self, reader: asyncio.StreamReader, ): self.reader: asyncio.StreamReader = reader self.started: bool = False self.lock = asyncio.Lock() @property def loop(self) -> asyncio.AbstractEventLoop: if not hasattr(self, "_loop"): self._loop = asyncio.get_event_loop() return self._loop def __aiter__(self) -> "FrameReceiver": return self async def get_frame(self) -> ReceivedFrame: if self.reader.at_eof(): del self.reader raise StopAsyncIteration with BytesIO() as fp: async with self.lock: try: fp.write(await self.reader.readexactly(1)) if fp.getvalue() == b"\0x00": fp.write(await self.reader.read()) raise InvalidFrameError(fp.getvalue()) if self.reader is None: raise AMQPConnectionError() fp.write(await self.reader.readexactly(6)) if not self.started and fp.getvalue().startswith(b"AMQP"): raise ProtocolSyntaxError( "Unexpected AMQP protocol header", ) else: self.started = True frame_type, _, frame_length = pamqp.frame.frame_parts( fp.getvalue(), ) if frame_length is None: raise InvalidFrameError("No frame length") fp.write(await self.reader.readexactly(frame_length + 1)) except asyncio.IncompleteReadError as e: raise AMQPConnectionError( "Server connection unexpectedly closed. " f"Read {len(e.partial)} bytes but {e.expected} " "bytes expected", ) from e except ConnectionRefusedError as e: raise AMQPConnectionError( f"Server connection refused: {e!r}", ) from e except ConnectionResetError as e: raise AMQPConnectionError( f"Server connection reset: {e!r}", ) from e except ConnectionError as e: raise AMQPConnectionError( f"Server connection error: {e!r}", ) from e except OSError as e: raise AMQPConnectionError( f"Server communication error: {e!r}", ) from e try: return pamqp.frame.unmarshal(fp.getvalue()) except PAMQPException as e: raise InvalidFrameError(str(e)) from e async def __anext__(self) -> ReceivedFrame: return await self.get_frame() class FrameGenerator(AsyncIterable): def __init__(self, queue: asyncio.Queue): self.queue: asyncio.Queue = queue self.close_event: asyncio.Event = asyncio.Event() def __aiter__(self) -> "FrameGenerator": return self async def __anext__(self) -> ChannelFrame: if self.close_event.is_set(): raise StopAsyncIteration frame: ChannelFrame = await self.queue.get() self.queue.task_done() return frame class SSLContextProvider: """Provides `ssl.SSLContext`. The context can be optionally provided at initialization by `ssl_context` arg. If it's not, the context is created using the certificate information provided in `ssl_certs` arg. """ def __init__( self, *, ssl_context: ssl.SSLContext | None, ssl_certs: SSLCerts, loop: asyncio.AbstractEventLoop, ) -> None: self._ssl_context = ssl_context self._ssl_certs = ssl_certs self._loop = loop async def get_context(self) -> ssl.SSLContext: """ Obtain `ssl.SSLContext` instance. If the context is provided at initialization, it is returned. Otherwise a new context is created using the provided certificate information. """ if self._ssl_context: return self._ssl_context ssl_context = await self._loop.run_in_executor( None, self._create_context ) self._ssl_context = ssl_context return ssl_context def _create_context(self) -> ssl.SSLContext: context = ssl.create_default_context( ssl.Purpose.SERVER_AUTH, capath=self._ssl_certs.capath, cafile=self._ssl_certs.cafile, cadata=self._ssl_certs.cadata, ) if self._ssl_certs.cert: context.load_cert_chain(self._ssl_certs.cert, self._ssl_certs.key) if not self._ssl_certs.verify: context.check_hostname = False context.verify_mode = ssl.CERT_NONE return context class TransportFactory(ABC): """ Abstract factory class allowing to open connections with generic transports. """ @abstractmethod async def create( self, url: URL, **kwargs: Any ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]: """Create a transport connection to the AMQP server.""" pass class TCPTransportFactory(TransportFactory): async def create( self, url: URL, **kwargs: Any ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]: # unexpected for asyncio.open_connection ignoring it _ = kwargs.pop("ssl_context_provider", None) try: return await asyncio.open_connection( host=url.host, port=url.port, ssl=None, **kwargs, ) except OSError as e: raise AMQPConnectionError(*e.args) from e class TLSTransportFactory(TransportFactory): async def create( self, url: URL, **kwargs: Any ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]: ssl_context_provider = kwargs.pop("ssl_context_provider") ssl = await ssl_context_provider.get_context() try: return await asyncio.open_connection( host=url.host, port=url.port, ssl=ssl, **kwargs, ) except OSError as e: raise AMQPConnectionError(*e.args) from e class Connection(Base, AbstractConnection): FRAME_BUFFER_SIZE = 10 # Interval between sending heartbeats based on the heartbeat(timeout) HEARTBEAT_INTERVAL_MULTIPLIER = 0.5 # Allow three missed heartbeats (based on heartbeat(timeout) HEARTBEAT_GRACE_MULTIPLIER = 3 READER_CLOSE_TIMEOUT = 2 _reader_task: TaskType _writer_task: TaskType __create_connection_kwargs: Mapping[str, Any] write_queue: asyncio.Queue server_properties: ArgumentsType connection_tune: spec.Connection.Tune channels: dict[int, AbstractChannel | None] @staticmethod def _parse_ca_data(data: str | None) -> bytes | None: return b64decode(data) if data else None def __init__( self, url: URLorStr, *, loop: asyncio.AbstractEventLoop | None = None, context: ssl.SSLContext | None = None, transport_factory: TransportFactory | None = None, client_properties: FieldTable | None = None, **create_connection_kwargs: Any, ): super().__init__(loop=loop or asyncio.get_event_loop(), parent=None) self.__client_properties: FieldTable = client_properties or {} self.url = URL(url) if self.url.is_absolute() and not self.url.port: self.url = self.url.with_port(DEFAULT_PORTS[self.url.scheme]) if self.url.path == "/" or not self.url.path: self.vhost = "/" else: quoted_vhost = self.url.path[1:] # yarl>=1.9.5 skips unquoting backslashes in path self.vhost = quoted_vhost.replace("%2F", "/") self.ssl_context = context self.ssl_certs = SSLCerts( cafile=self.url.query.get("cafile"), capath=self.url.query.get("capath"), cadata=self._parse_ca_data(self.url.query.get("cadata")), key=self.url.query.get("keyfile"), cert=self.url.query.get("certfile"), verify=self.url.query.get("no_verify_ssl", "0") == "0", ) self.started = False self.channels = {} self.write_queue = asyncio.Queue( maxsize=self.FRAME_BUFFER_SIZE, ) self.last_channel = 1 self.timeout = parse_int(self.url.query.get("timeout", "60")) self.heartbeat_timeout = parse_heartbeat( self.url.query.get("heartbeat", "60"), ) self.last_channel_lock = asyncio.Lock() self.connected = asyncio.Event() self.connection_name = self.url.query.get("name") if transport_factory: self._transport_factory = transport_factory elif self.url.scheme == "amqps": self._transport_factory = TLSTransportFactory() else: self._transport_factory = TCPTransportFactory() self.__close_reply_code: int = REPLY_SUCCESS self.__close_reply_text: str = "normally closed" self.__close_class_id: int = 0 self.__close_method_id: int = 0 self.__update_secret_lock: asyncio.Lock = asyncio.Lock() self.__update_secret_future: asyncio.Future | None = None self.__connection_unblocked: asyncio.Event = asyncio.Event() self.__heartbeat_grace_timeout = ( (self.heartbeat_timeout + 1) * self.HEARTBEAT_GRACE_MULTIPLIER ) self.__last_frame_time: float = self.loop.time() self.__create_connection_kwargs = create_connection_kwargs async def ready(self) -> None: await self.connected.wait() await self.__connection_unblocked.wait() def set_close_reason( self, reply_code: int = REPLY_SUCCESS, reply_text: str = "normally closed", class_id: int = 0, method_id: int = 0, ) -> None: self.__close_reply_code = reply_code self.__close_reply_text = reply_text self.__close_class_id = class_id self.__close_method_id = method_id @property def is_opened(self) -> bool: is_reader_running = ( hasattr(self, "_reader_task") and not self._reader_task.done() ) is_writer_running = ( hasattr(self, "_writer_task") and not self._writer_task.done() ) return ( is_reader_running and is_writer_running and not self.is_closed ) def __str__(self) -> str: return str(censor_url(self.url)) def _client_properties(self, **kwargs: Any) -> dict[str, Any]: properties = { "platform": PLATFORM, "version": __version__, "product": PRODUCT, "capabilities": { "authentication_failure_close": True, "basic.nack": True, "connection.blocked": True, "consumer_cancel_notify": True, "publisher_confirms": True, }, "information": "See https://github.com/mosquito/aiormq/", } properties.update( parse_connection_name(self.connection_name), ) properties.update(kwargs) return properties def _credentials_class( self, start_frame: spec.Connection.Start, ) -> AuthMechanism: auth_requested = self.url.query.get("auth", "plain").upper() auth_available = start_frame.mechanisms.split() if auth_requested in auth_available: with suppress(KeyError): return AuthMechanism[auth_requested] raise AuthenticationError( start_frame.mechanisms, [m.name for m in AuthMechanism], ) @staticmethod async def _rpc( request: Frame, writer: asyncio.StreamWriter, frame_receiver: FrameReceiver, wait_response: bool = True, ) -> FrameTypes | None: writer.write(pamqp.frame.marshal(request, 0)) await writer.drain() if not wait_response: return None _, _, frame = await frame_receiver.get_frame() if request.synchronous and frame.name not in request.valid_responses: raise InvalidFrameError( f"Expected one of {request.valid_responses!r}, got {frame!r}", ) elif isinstance(frame, spec.Connection.Close): if frame.reply_code == 403: raise ProbableAuthenticationError(frame.reply_text) raise ConnectionClosed(frame.reply_code, frame.reply_text) return frame @task async def connect( self, client_properties: FieldTable | None = None, ) -> bool: if self.is_opened: raise RuntimeError("Connection already opened") log.debug("Connecting to: %s", self) try: reader, writer = await self._transport_factory.create( self.url, ssl_context_provider=SSLContextProvider( ssl_context=self.ssl_context, ssl_certs=self.ssl_certs, loop=self.loop ), **self.__create_connection_kwargs, ) except Exception as e: log.error("error when creating transport: %r", e) raise e frame_receiver = FrameReceiver(reader) frame: FrameTypes | None # Every failure after the transport exists must close the writer, # the protocol header exchange included. try: protocol_header = ProtocolHeader() writer.write(protocol_header.marshal()) try: _, _, frame = await frame_receiver.get_frame() except EOFError as e: raise IncompatibleProtocolError(*e.args) from e if not isinstance(frame, spec.Connection.Start): raise InvalidFrameError( f"Expected Connection.Start, got {frame!r}", ) credentials = self._credentials_class(frame) server_properties: ArgumentsType = frame.server_properties if client_properties is None: client_properties = self.__client_properties frame = await self._rpc( spec.Connection.StartOk( client_properties=self._client_properties( **client_properties, ), mechanism=credentials.name, response=credentials.value(self).marshal(), ), writer=writer, frame_receiver=frame_receiver, ) if not isinstance(frame, spec.Connection.Tune): raise InvalidFrameError( f"Expected Connection.Tune, got {frame!r}", ) connection_tune: spec.Connection.Tune = frame connection_tune.heartbeat = self.heartbeat_timeout await self._rpc( spec.Connection.TuneOk( channel_max=connection_tune.channel_max, frame_max=connection_tune.frame_max, heartbeat=connection_tune.heartbeat, ), writer=writer, frame_receiver=frame_receiver, wait_response=False, ) frame = await self._rpc( spec.Connection.Open(virtual_host=self.vhost), writer=writer, frame_receiver=frame_receiver, ) if not isinstance(frame, spec.Connection.OpenOk): raise InvalidFrameError( f"Expected Connection.OpenOk, got {frame!r}", ) except BaseException as e: await self.__close_writer(writer) await self.close(e) raise # noinspection PyAsyncCall self._reader_task = self.create_task(self.__reader(frame_receiver)) self._reader_task.add_done_callback(self._on_reader_done) # noinspection PyAsyncCall self._writer_task = self.create_task(self.__writer(writer)) self.connection_tune = connection_tune self.server_properties = server_properties return True def _on_reader_done(self, task: asyncio.Task) -> None: log.debug("Reader exited for %r", self) if not task.cancelled() and task.exception() is not None: # Show the cause. A debug record without the traceback hides # a malformed frame from the broker. log.warning( "Cancelling cause reader exited abnormally", exc_info=task.exception(), ) self.set_close_reason( reply_code=500, reply_text="reader unexpected closed", ) async def close_writer_task() -> None: if not self._writer_task.done(): # A second cancellation would interrupt wait_closed(), # leaving the TLS transport open during shutdown. if not self._writer_task.cancelling(): self._writer_task.cancel() await asyncio.gather(self._writer_task, return_exceptions=True) try: exc = task.exception() except asyncio.CancelledError as e: exc = e await self.close(exc) self.loop.create_task(close_writer_task()) async def __handle_close_ok(self, _: spec.Connection.CloseOk) -> None: return async def __handle_heartbeat(self, _: Heartbeat) -> None: return async def __handle_close(self, frame: spec.Connection.Close) -> None: log.error( "Unexpected connection close from remote \"%s\", " "Connection.Close(reply_code=%r, reply_text=%r)", self, frame.reply_code, frame.reply_text, ) with suppress(asyncio.QueueFull): self.write_queue.put_nowait( ChannelFrame.marshall( channel_number=0, frames=[spec.Connection.CloseOk()], ), ) exception = exception_by_code(frame) if ( self.__update_secret_future is not None and not self.__update_secret_future.done() ): self.__update_secret_future.set_exception(exception) raise exception async def __handle_channel_close_ok( self, _: spec.Channel.CloseOk, ) -> None: self.channels.pop(0, None) async def __handle_channel_update_secret_ok( self, frame: spec.Connection.UpdateSecretOk, ) -> None: if ( self.__update_secret_future is not None and not self.__update_secret_future.done() ): self.__update_secret_future.set_result(frame) return log.warning("Got unexpected UpdateSecretOk frame") async def __handle_connection_blocked( self, frame: spec.Connection.Blocked, ) -> None: log.warning("Connection %r was blocked by: %r", self, frame.reason) self.__connection_unblocked.clear() async def __handle_connection_unblocked( self, _: spec.Connection.Unblocked, ) -> None: log.warning("Connection %r was unblocked", self) self.__connection_unblocked.set() async def __reader(self, frame_receiver: FrameReceiver) -> None: self.__connection_unblocked.set() self.connected.set() # Not very optimal, but avoid creating a task for each frame sending # noinspection PyAsyncCall if self.heartbeat_timeout > 0: self.create_task(self.__heartbeat()) try: async for weight, channel, frame in frame_receiver: self.__last_frame_time = self.loop.time() log.debug( "Received frame %r in channel #%d weight=%s on %r", frame, channel, weight, self, ) if channel == 0: match frame: case spec.Connection.CloseOk(): await self.__handle_close_ok(frame) # Close-Ok is the last frame on the connection. # The writer waits for the reader to stop # before it closes the socket. return case spec.Connection.Close(): await self.__handle_close(frame) case Heartbeat(): await self.__handle_heartbeat(frame) case spec.Channel.CloseOk(): await self.__handle_channel_close_ok(frame) case spec.Connection.UpdateSecretOk(): await self.__handle_channel_update_secret_ok(frame) case spec.Connection.Blocked(): await self.__handle_connection_blocked(frame) case spec.Connection.Unblocked(): await self.__handle_connection_unblocked(frame) case _: log.error("Unexpected frame %r", frame) continue ch: AbstractChannel | None = self.channels.get(channel) if ch is None: log.error( "Got frame for closed channel %d: %r", channel, frame, ) continue if isinstance(frame, CHANNEL_CLOSE_RESPONSES): # The broker confirmed the close. The number is free # even when the channel object is already closed. self.channels.pop(channel, None) await ch.frames.put((weight, frame)) except asyncio.CancelledError: if self.is_connection_was_stuck: log.warning( "Server connection %r was stuck. No frames were received " "in %d seconds.", self, self.__heartbeat_grace_timeout, ) self._writer_task.cancel() raise @property def is_connection_was_stuck(self) -> bool: delay = self.loop.time() - self.__last_frame_time return delay > self.__heartbeat_grace_timeout async def __heartbeat(self) -> None: heartbeat_timeout = max(1, self.heartbeat_timeout // 2) heartbeat = ChannelFrame.marshall( frames=[Heartbeat()], channel_number=0, ) while not self._closing.done(): if self.is_connection_was_stuck: self._reader_task.cancel() return await asyncio.sleep(heartbeat_timeout) try: await asyncio.wait_for( self.write_queue.put(heartbeat), timeout=self.__heartbeat_grace_timeout, ) except asyncio.TimeoutError: self._reader_task.cancel() return async def __writer(self, writer: asyncio.StreamWriter) -> None: channel_frame: ChannelFrame try: frame_iterator = FrameGenerator(self.write_queue) self._closing.add_done_callback( lambda _: frame_iterator.close_event.set(), ) if not self.__connection_unblocked.is_set(): await self.__connection_unblocked.wait() async for channel_frame in frame_iterator: log.debug("Prepare to send %r", channel_frame) writer.write(channel_frame.payload) if channel_frame.should_close: await writer.drain() channel_frame.drain() return if channel_frame.should_drain: await writer.drain() channel_frame.drain() except asyncio.CancelledError: if not self.__check_writer(writer) or self.is_connection_was_stuck: raise frame = spec.Connection.Close( reply_code=self.__close_reply_code, reply_text=self.__close_reply_text, class_id=self.__close_class_id, method_id=self.__close_method_id, ) writer.write(ChannelFrame.marshall(0, [frame]).payload) log.debug("Sending %r to %r", frame, self) try: await asyncio.wait_for( writer.drain(), timeout=self.__heartbeat_grace_timeout, ) # The broker must answer with Connection.Close-Ok before # the socket is closed, or it logs an unexpected close. # The reader stops after it gets Close-Ok. if not self._reader_task.done(): await asyncio.wait( [asyncio.gather( self._reader_task, return_exceptions=True, )], timeout=self.READER_CLOSE_TIMEOUT, ) finally: # Stop the reader before the socket is closed, so it does # not fail on EOF. if not self._reader_task.done(): self._reader_task.cancel() await self.__close_writer(writer) raise finally: if not writer.is_closing(): await self.__close_writer(writer) log.debug("Writer exited for %r", self) if sys.version_info < (3, 7): async def __close_writer(self, writer: asyncio.StreamWriter) -> None: log.debug("Writer on connection %s closed", self) writer.close() else: async def __close_writer(self, writer: asyncio.StreamWriter) -> None: log.debug("Writer on connection %s closed", self) with suppress(OSError, RuntimeError): if writer.can_write_eof(): writer.write_eof() writer.close() if self.is_connection_was_stuck: # The peer cannot complete the TLS shutdown handshake. writer.transport.abort() with suppress(OSError, RuntimeError): await writer.wait_closed() @staticmethod def __check_writer(writer: asyncio.StreamWriter) -> bool: if writer is None: return False if hasattr(writer, "is_closing"): return not writer.is_closing() if writer.transport: return not writer.transport.is_closing() return writer.can_write_eof() async def _on_close( self, exc: ExceptionType | None = ConnectionClosed(0, "normal closed"), ) -> None: log.debug("Closing connection %r cause: %r", self, exc) if ( not self._writer_task.done() and not self._writer_task.cancelling() ): # Stop the writer first. It sends Connection.Close and waits # for the reader to get Connection.Close-Ok. self._writer_task.cancel() await asyncio.gather(self._writer_task, return_exceptions=True) if not self._reader_task.done(): self._reader_task.cancel() await asyncio.gather( self._reader_task, self._writer_task, return_exceptions=True, ) if self._closing.done(): return if exc is None: self._closing.set_result(None) else: self._closing.set_exception(exc) @property def server_capabilities(self) -> ArgumentsType: return self.server_properties["capabilities"] # type: ignore @property def basic_nack(self) -> bool: return bool(self.server_capabilities.get("basic.nack")) @property def consumer_cancel_notify(self) -> bool: return bool(self.server_capabilities.get("consumer_cancel_notify")) @property def accept_consumer_cancel_ok(self) -> bool: """The broker accepts a client answer to its own Basic.Cancel. RabbitMQ 4.3.7 and later advertise this capability. Older brokers close the channel on an unexpected Basic.CancelOk. """ return bool( self.server_capabilities.get("accept_consumer_cancel_ok"), ) @property def exchange_exchange_bindings(self) -> bool: return bool(self.server_capabilities.get("exchange_exchange_bindings")) @property def publisher_confirms(self) -> bool | None: publisher_confirms = self.server_capabilities.get("publisher_confirms") if publisher_confirms is None: return None return bool(publisher_confirms) async def channel( self, channel_number: int | None = None, publisher_confirms: bool = True, frame_buffer_size: int = FRAME_BUFFER_SIZE, timeout: TimeoutType = None, **kwargs: Any, ) -> AbstractChannel: await self.connected.wait() if self.is_closed: raise RuntimeError("%r closed" % self) if not self.publisher_confirms and publisher_confirms: raise ValueError("Server doesn't support publisher_confirms") if channel_number is None: async with self.last_channel_lock: if self.channels: self.last_channel = max(self.channels.keys()) while self.last_channel in self.channels.keys(): self.last_channel += 1 if self.last_channel > 65535: log.warning("Resetting channel number for %r", self) self.last_channel = 1 # switching context for prevent blocking event-loop await asyncio.sleep(0) channel_number = self.last_channel elif channel_number in self.channels: raise ValueError("Channel %d already used" % channel_number) if channel_number < 0 or channel_number > 65535: raise ValueError("Channel number too large") channel = Channel( self, channel_number, frame_buffer=frame_buffer_size, publisher_confirms=publisher_confirms, **kwargs, ) self.channels[channel_number] = channel # Keep the number until the broker confirms that the channel is closed. # Release the number immediately if Channel.Open was not sent. await channel.open(timeout=timeout) return channel async def update_secret( self, new_secret: str, *, reason: str = "", timeout: TimeoutType = None, ) -> spec.Connection.UpdateSecretOk: channel_frame = ChannelFrame.marshall( channel_number=0, frames=[ spec.Connection.UpdateSecret( new_secret=new_secret, reason=reason, ), ], ) countdown = Countdown(timeout) async with countdown.enter_context(self.__update_secret_lock): self.__update_secret_future = self.loop.create_future() await self.write_queue.put(channel_frame) try: response: spec.Connection.UpdateSecretOk = ( await countdown(self.__update_secret_future) ) finally: self.__update_secret_future = None return response async def __aenter__(self) -> AbstractConnection: if not self.is_opened: await self.connect() return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self.close(exc_val) class ConnectionContext(Coroutine[Any, Any, Connection]): """Result of aiormq.connect(). `async with aiormq.connect(url) as connection:` opens the connection and closes it on exit. `await aiormq.connect(url)` opens it and returns it, as before aiormq 7.1. The object follows the coroutine protocol, so asyncio.create_task(), asyncio.run() and similar callers accept it like the coroutine that connect() returned before. """ __slots__ = ("_args", "_kwargs", "_connection", "_coro") def __init__(self, *args: Any, **kwargs: Any): self._args = args self._kwargs = kwargs self._connection: Connection | None = None self._coro: Coroutine[Any, Any, Connection] | None = None @property def connection(self) -> Connection: # The Connection is built on first use. Its constructor needs an # event loop, and asyncio.run(aiormq.connect(url)) has none yet # when connect() is called. if self._connection is None: self._connection = Connection(*self._args, **self._kwargs) return self._connection async def __aenter__(self) -> Connection: await self.connection.__aenter__() return self.connection async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self.connection.__aexit__(exc_type, exc_val, exc_tb) async def __connect(self) -> Connection: await self.connection.connect() return self.connection def __coroutine(self) -> Coroutine[Any, Any, Connection]: if self._coro is None: self._coro = self.__connect() return self._coro def __await__(self) -> Generator[Any, None, Connection]: return self.__coroutine().__await__() def send(self, value: Any) -> Any: return self.__coroutine().send(value) def throw(self, *args: Any) -> Any: return self.__coroutine().throw(*args) def close(self) -> None: if self._coro is not None: self._coro.close() def connect( url: URLorStr, *args: Any, client_properties: FieldTable | None = None, **kwargs: Any, ) -> ConnectionContext: """Prepare a connection. See ConnectionContext for the ways to open it.""" return ConnectionContext( url, *args, client_properties=client_properties, **kwargs, ) # connect() was a coroutine function before aiormq 7.1. Keep # inspect.iscoroutinefunction() and asyncio.iscoroutinefunction() true. if hasattr(inspect, "markcoroutinefunction"): # Python 3.12+ connect = inspect.markcoroutinefunction(connect) connect._is_coroutine = asyncio.coroutines._is_coroutine # ty: ignore[unresolved-attribute] # noqa: E501 mosquito-aiormq-612e0d0/aiormq/exceptions.py000066400000000000000000000151701527161700100212320ustar00rootroot00000000000000from typing import Any from pamqp.base import Frame from pamqp.commands import Basic from .abc import DeliveredMessage class AMQPError(Exception): reason = "An unspecified AMQP error has occurred: %s" def __repr__(self) -> str: try: return "<%s: %s>" % ( self.__class__.__name__, self.reason % self.args, ) except TypeError: # FIXME: if you are here file an issue return f"<{self.__class__.__name__}: {self.args!r}>" # Backward compatibility AMQPException = AMQPError class AMQPConnectionError(AMQPError, ConnectionError): reason = "Unexpected connection problem" def __repr__(self) -> str: if self.args: return f"<{self.__class__.__name__}: {self.args!r}>" return AMQPError.__repr__(self) class IncompatibleProtocolError(AMQPConnectionError): reason = "The protocol returned by the server is not supported" class AuthenticationError(AMQPConnectionError): reason = ( "Server and client could not negotiate use of the " "authentication mechanisms. Server supports only %r, " "but client supports only %r." ) class ProbableAuthenticationError(AMQPConnectionError): reason = ( "Client was disconnected at a connection stage indicating a " "probable authentication error: %s" ) class ConnectionClosed(AMQPConnectionError): reason = "The AMQP connection was closed (%s) %s" class ConnectionSyntaxError(ConnectionClosed): reason = ( "The sender sent a frame that contained illegal values for " "one or more fields. This strongly implies a programming error " "in the sending peer: %r" ) class ConnectionFrameError(ConnectionClosed): reason = ( "The sender sent a malformed frame that the recipient could " "not decode. This strongly implies a programming error " "in the sending peer: %r" ) class ConnectionCommandInvalid(ConnectionClosed): reason = ( "The client sent an invalid sequence of frames, attempting to " "perform an operation that was considered invalid by the server." " This usually implies a programming error in the client: %r" ) class ConnectionChannelError(ConnectionClosed): reason = ( "The client attempted to work with a channel that had not been " "correctly opened. This most likely indicates a fault in the " "client layer: %r" ) class ConnectionUnexpectedFrame(ConnectionClosed): reason = ( "The peer sent a frame that was not expected, usually in the " "context of a content header and body. This strongly indicates " "a fault in the peer's content processing: %r" ) class ConnectionResourceError(ConnectionClosed): reason = ( "The server could not complete the method because it lacked " "sufficient resources. This may be due to the client creating " "too many of some type of entity: %r" ) class ConnectionNotAllowed(ConnectionClosed): reason = ( "The client tried to work with some entity in a manner that is " "prohibited by the server, due to security settings or by " "some other criteria: %r" ) class ConnectionNotImplemented(ConnectionClosed): reason = ( "The client tried to use functionality that is " "not implemented in the server: %r" ) class ConnectionInternalError(ConnectionClosed): reason = ( " The server could not complete the method because of an " "internal error. The server may require intervention by an " "operator in order to resume normal operations: %r" ) class AMQPChannelError(AMQPError): reason = "An unspecified AMQP channel error has occurred" class ChannelClosed(AMQPChannelError): reason = "The channel was closed (%s) %s" class ChannelAccessRefused(ChannelClosed): reason = ( "The client attempted to work with a server entity to " "which it has no access due to security settings: %r" ) class ChannelNotFoundEntity(ChannelClosed): reason = ( "The client attempted to work with a server " "entity that does not exist: %r" ) class ChannelLockedResource(ChannelClosed): reason = ( "The client attempted to work with a server entity to " "which it has no access because another client is working " "with it: %r" ) class ChannelPreconditionFailed(ChannelClosed): reason = ( "The client requested a method that was not allowed because " "some precondition failed: %r" ) class DuplicateConsumerTag(ChannelClosed): reason = "The consumer tag specified already exists for this channel: %s" class ProtocolSyntaxError(AMQPError): reason = "An unspecified protocol syntax error occurred" class InvalidFrameError(ProtocolSyntaxError): reason = "Invalid frame received: %r" class MethodNotImplemented(AMQPError): pass class DeliveryError(AMQPError): __slots__ = "message", "frame" reason = "Error when delivery message %r, frame %r" def __init__( self, message: DeliveredMessage | None, frame: Frame, *args: Any, ): self.message = message self.frame = frame super().__init__(self.message, self.frame) def __str__(self) -> str: frame = self.frame match frame: case Basic.Nack(): details = ( f"delivery_tag={frame.delivery_tag!r}, " f"multiple={frame.multiple!r}" ) case Basic.Reject(): details = ( f"delivery_tag={frame.delivery_tag!r}, " f"requeue={frame.requeue!r}" ) case Basic.Return(): details = ( f"reply_code={frame.reply_code!r}, " f"reply_text={frame.reply_text!r}, " f"exchange={frame.exchange!r}, " f"routing_key={frame.routing_key!r}" ) case _: details = "" return f"Message delivery failed: {frame.name}({details})" def __repr__(self) -> str: return f"<{type(self).__name__}: {self}>" class PublishError(DeliveryError): reason = "%r for routing key %r" def __init__(self, message: DeliveredMessage, frame: Frame, *args: Any): assert isinstance(message.delivery, Basic.Return) self.message = message self.frame = frame super(DeliveryError, self).__init__( message.delivery.reply_text, message.delivery.routing_key, *args, ) class ChannelInvalidStateError(RuntimeError): pass mosquito-aiormq-612e0d0/aiormq/py.typed000066400000000000000000000000001527161700100201600ustar00rootroot00000000000000mosquito-aiormq-612e0d0/aiormq/tools.py000066400000000000000000000065111527161700100202100ustar00rootroot00000000000000import asyncio import inspect import platform import time from collections.abc import Awaitable, Callable, Coroutine from contextlib import AbstractAsyncContextManager from functools import wraps from types import TracebackType from typing import Any, TypeVar from yarl import URL from aiormq.abc import TimeoutType T = TypeVar("T") def censor_url(url: URL) -> URL: if url.password is not None: return url.with_password("******") return url def shield(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]: @wraps(func) def wrap(*args: Any, **kwargs: Any) -> Awaitable[T]: return asyncio.shield(func(*args, **kwargs)) return wrap def awaitable( func: Callable[..., T | Awaitable[T]], ) -> Callable[..., Coroutine[Any, Any, T]]: # Avoid python 3.8+ warning if inspect.iscoroutinefunction(func): return func @wraps(func) async def wrap(*args: Any, **kwargs: Any) -> T: result = func(*args, **kwargs) if hasattr(result, "__await__"): return await result # type: ignore if asyncio.iscoroutine(result) or asyncio.isfuture(result): return await result return result return wrap class Countdown: __slots__ = "loop", "deadline" if platform.system() == "Windows": @staticmethod def _now() -> float: # windows monotonic timer resolution is not enough. # Have to use time.time() return time.time() else: @staticmethod def _now() -> float: return time.monotonic() def __init__(self, timeout: TimeoutType = None): self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() self.deadline: TimeoutType = None if timeout is not None: self.deadline = self._now() + timeout def get_timeout(self) -> TimeoutType: if self.deadline is None: return None current = self._now() if current >= self.deadline: raise asyncio.TimeoutError return self.deadline - current async def __call__(self, coro: Awaitable[T]) -> T: try: timeout = self.get_timeout() except asyncio.TimeoutError: fut = asyncio.ensure_future(coro) fut.cancel() await asyncio.gather(fut, return_exceptions=True) raise if self.deadline is None and not timeout: return await coro return await asyncio.wait_for(coro, timeout=timeout) def enter_context( self, ctx: AbstractAsyncContextManager[T], ) -> AbstractAsyncContextManager[T]: return CountdownContext(self, ctx) class CountdownContext(AbstractAsyncContextManager): def __init__(self, countdown: Countdown, ctx: AbstractAsyncContextManager): self.countdown: Countdown = countdown self.ctx: AbstractAsyncContextManager = ctx async def __aenter__(self) -> T: return await self.countdown(self.ctx.__aenter__()) async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> Any: # Do not apply the deadline here. An expired deadline must not # skip the exit of the inner context, or a lock stays acquired. return await self.ctx.__aexit__(exc_type, exc_val, exc_tb) mosquito-aiormq-612e0d0/aiormq/types.py000066400000000000000000000003471527161700100202150ustar00rootroot00000000000000import warnings from .abc import * # noqa warnings.warn( "aiormq.types was deprecated and will be removed in " "one of next major releases. Use aiormq.abc instead.", category=DeprecationWarning, stacklevel=2, ) mosquito-aiormq-612e0d0/conftest.py000066400000000000000000000115271527161700100174100ustar00rootroot00000000000000"""Session fixtures shared by tests/ and README.md. The RabbitMQ broker comes from AMQP_URL when it is set (CI service container). Otherwise a docker container is started for the session. """ import asyncio import atexit import os import socket from contextlib import suppress from time import monotonic, sleep from typing import Any, Awaitable, Callable, Generator import pytest from yarl import URL from tests.docker_client import ( ContainerInfo, DockerClient, DockerHostInfo, DockerNotAvailableError, check_docker_available, ) # Cached docker host info from pytest_configure _docker_host_info: DockerHostInfo | None = None # Global registry for atexit cleanup _docker_client: DockerClient | None = None _docker_containers: set[str] = set() def _atexit_kill_containers() -> None: """Kill all containers on exit (handles crashes/interrupts).""" if _docker_client is None: return for container_id in _docker_containers: with suppress(Exception): _docker_client.kill(container_id) with suppress(Exception): _docker_client.remove(container_id) _docker_containers.clear() atexit.register(_atexit_kill_containers) def pytest_configure(config: pytest.Config) -> None: """Check Docker availability before running tests.""" global _docker_host_info if os.environ.get("AMQP_URL"): return try: _docker_host_info = check_docker_available() except DockerNotAvailableError as e: raise pytest.UsageError(str(e)) from e @pytest.fixture(scope="session") def docker() -> Generator[Callable[..., ContainerInfo], Any, Any]: global _docker_client _docker_client = DockerClient(_docker_host_info) def docker_run( image: str, ports: list[str], environment: dict[str, str] | None = None, ) -> ContainerInfo: info = _docker_client.run(image, ports, environment=environment) _docker_containers.add(info.id) return info try: yield docker_run finally: for container_id in list(_docker_containers): with suppress(Exception): _docker_client.kill(container_id) with suppress(Exception): _docker_client.remove(container_id) _docker_containers.discard(container_id) def wait_for_broker(host: str, port: int, timeout: float = 60.0) -> None: """Block until the broker answers the AMQP protocol header.""" deadline = monotonic() + timeout while True: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(5) try: sock.connect((host, port)) sock.send(b"AMQP\x00\x00\x09\x01") if len(sock.recv(4)) == 4: return except OSError: pass if monotonic() > deadline: pytest.fail(f"RabbitMQ at {host}:{port} is not ready") sleep(0.3) @pytest.fixture(scope="session") def rabbitmq_container( docker: Callable[..., ContainerInfo], ) -> ContainerInfo: amqp_url = os.environ.get("AMQP_URL") if amqp_url: url = URL(amqp_url) info = ContainerInfo( id="ci-service", ports={ "5672/tcp": url.port or 5672, "5671/tcp": 5671, "15672/tcp": 15672, "15671/tcp": 15671, }, host=url.host or "localhost", ) else: info = docker( "mosquito/aiormq-rabbitmq", ["5672/tcp", "5671/tcp", "15672/tcp", "15671/tcp"], ) # A CI service container can accept TCP before the broker listens. wait_for_broker(info.host, info.ports["5672/tcp"]) return info @pytest.fixture(scope="session") def amqp_direct_url(rabbitmq_container: ContainerInfo) -> URL: return URL.build( scheme="amqp", user="guest", password="guest", path="//", host=rabbitmq_container.host, port=rabbitmq_container.ports["5672/tcp"], ) @pytest.fixture(scope="session") def amqp_url(amqp_direct_url: URL) -> str: """Broker URL for README.md examples. tests/ override this fixture.""" return str(amqp_direct_url) @pytest.fixture def wait_for_output( capsys: pytest.CaptureFixture[str], ) -> Callable[..., Awaitable[None]]: """Return a coroutine function that waits for text in captured stdout. README.md consumer examples print from callbacks. The hidden test blocks use this helper to wait for the callback instead of a sleep. """ async def wait(text: str, timeout: float = 10.0) -> None: deadline = monotonic() + timeout output = "" while text not in output: output += capsys.readouterr().out if monotonic() > deadline: pytest.fail(f"{text!r} not found in output: {output!r}") await asyncio.sleep(0.05) return wait mosquito-aiormq-612e0d0/pyproject.toml000066400000000000000000000053041527161700100201210ustar00rootroot00000000000000[project] name = "aiormq" version = "7.0.0" description = "Pure python AMQP asynchronous client library" authors = [{ name = "Dmitry Orlov", email = "me@mosquito.su" }] requires-python = ">=3.11,<4" readme = "README.md" license = "Apache-2.0" license-files = ["COPYING"] keywords = [ "rabbitmq", "asyncio", "amqp", "amqp 0.9.1", "driver", "pamqp", ] classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Topic :: Internet", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: System :: Clustering", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: Microsoft", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Free Threading :: 3 - Stable", ] dependencies = [ "pamqp>=5,<6", "yarl", ] [project.urls] Homepage = "https://github.com/mosquito/aiormq" Source = "https://github.com/mosquito/aiormq" Tracker = "https://github.com/mosquito/aiormq/issues" Documentation = "https://github.com/mosquito/aiormq/blob/master/README.md" [dependency-groups] dev = [ "pytest>=8.4,<10", "coverage>=6.5,<8", "coveralls>=3.3.1,<5", "ruff", "pytest-cov>=4,<8", "markdown-pytest>=0.6.6,<0.7", "aiomisc-pytest>=2,<3", "ty>=0.0.84", ] uvloop = ["uvloop>=0.23,<0.24"] # An empty group runs the tests on the asyncio event loop. asyncio = [] [tool.uv] default-groups = "all" [tool.uv.build-backend] module-name = ["aiormq"] module-root = "" source-include = ["tests/**"] [build-system] requires = ["uv_build>=0.12.19,<0.13"] build-backend = "uv_build" [tool.ty.src] include = ["aiormq"] [tool.ty.environment] python-version = "3.11" [tool.pytest.ini_options] addopts = "-p no:asyncio" markers = [ "slow: marks tests as slow (deselect with '-m \"not slow\"')", ] [tool.coverage.run] branch = true [tool.coverage.report] exclude_lines = [ "pragma: no cover", "raise NotImplementedError", ] [tool.ruff] line-length = 80 exclude = [ ".git", ".tox", ".venv", "build", "dist", "env", ] [tool.ruff.lint] select = ["E", "W", "F", "C90"] ignore = ["C901"] mosquito-aiormq-612e0d0/tests/000077500000000000000000000000001527161700100163455ustar00rootroot00000000000000mosquito-aiormq-612e0d0/tests/__init__.py000066400000000000000000000000001527161700100204440ustar00rootroot00000000000000mosquito-aiormq-612e0d0/tests/certs/000077500000000000000000000000001527161700100174655ustar00rootroot00000000000000mosquito-aiormq-612e0d0/tests/certs/Dockerfile000066400000000000000000000007671527161700100214710ustar00rootroot00000000000000# Test broker for aiormq. Build from the repository root: # # make rabbitmq-image # # RabbitMQ 4.3.7 and later advertise the accept_consumer_cancel_ok # capability, which the consumer cancel tests require. FROM rabbitmq:4.4-management-alpine RUN mkdir -p /certs/ COPY tests/certs/ca.pem /certs/ COPY tests/certs/server.key /certs/ COPY tests/certs/server.pem /certs/ COPY tests/certs/rabbitmq.conf /etc/rabbitmq/conf.d/20-aiormq.conf ENV RABBITMQ_DEFAULT_USER=guest ENV RABBITMQ_DEFAULT_PASS=guest mosquito-aiormq-612e0d0/tests/certs/ca.pem000066400000000000000000000041221527161700100205520ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIF+DCCA+CgAwIBAgIBATANBgkqhkiG9w0BAQ0FADCBgzELMAkGA1UEBhMCUlUx DzANBgNVBAgTBk1vc2NvdzEPMA0GA1UEBxMGTW9zY293MQ0wCwYDVQQKEwRIb21l MRAwDgYDVQQLEwdJVCBDcmV3MRAwDgYDVQQDEwdUZXN0IENBMR8wHQYJKoZIhvcN AQkBFhByb290QGV4YW1wbGUuY29tMCAXDTE4MTIxNTExMzEwMFoYDzIyMTgxMjE1 MTEzMTAwWjCBgzELMAkGA1UEBhMCUlUxDzANBgNVBAgTBk1vc2NvdzEPMA0GA1UE BxMGTW9zY293MQ0wCwYDVQQKEwRIb21lMRAwDgYDVQQLEwdJVCBDcmV3MRAwDgYD VQQDEwdUZXN0IENBMR8wHQYJKoZIhvcNAQkBFhByb290QGV4YW1wbGUuY29tMIIC IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2PtC5aCUO3dj1H6rK3pXwUFY msIMB6uI3cKqx4U3thyL3orTAYu51Ax/nG8iVi9X3CY0v0Bfwrq004oqCFuyygl0 yNRKomx9prpunCRv+vW6ojpif+iMOJmyGKQ8vhMSCbUgbk2Z53U1FKYWybPk9bXA fpI1KdyT2iVB0wDKKLkbLTtMuSOiFDCYZK5+yVBhbxIBQtoldhejbgHh0z7r78Bz v5vRwTyL73xHJydj+7yxJp4BgcptGYMJO6pb+c8vbLBdQy38i1vAAQa1XQzh8jUU KMXsO6LssMAdVMpA53uscQNz8j7g+cGwnWPe2t1f8I81tbI/oZ3MAf42zSAfbvde x9LbaXmmYAcifqdkkHaaaTKr8jVZyxK/CKMUzsL7JckDxXMAQgoKofWespyi2cqF /XISEnFaqOFh5brxwZmIKy2/GAqpyIv2BPWefgEhw1+d6GZMysPiSZKOdGUtoqlW 1Ni55z/gKLYNcTRyah7cGP6tGasSFjZAvMmgW6I+Q559pUXfsCSI+MGzGgeBNiPX 460j4qfyM/2kvR2vIjtY5choV2utDk3XMr5jSPatFKPX0K136TTMqEgBHL3SQ228 apR18MrNYwnlOSxXsr/85s/Hf7dtk7OPRIxJPiewUnUK2UrLQ8eLYdSMjJgJxfmc 2a2DQwq/0MwQomcJbtECAwEAAaNzMHEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E FgQUcylnYt/dChvaK5Ndc/Ko25dXrfswCwYDVR0PBAQDAgEGMBEGCWCGSAGG+EIB AQQEAwIABzAfBglghkgBhvhCAQ0EEhYQdGVzdCBjZXJ0aWZpY2F0ZTANBgkqhkiG 9w0BAQ0FAAOCAgEATHN9WJedxwW3bDr5yO1BgOVGYaGpbg2DheHnL61smQwimfZm T8z/1mn9iKBJd++lh/lfCTINlX4N3ViCTgDx+dEilQ80RQlbpi5fcrsU0xbuBHBX Pn/I61CuGebfFMQLD8qazkuZ7SDZMA5LdmPL7FnIJpHdl7DnnghyLHuakLn/7Qlu UDh8WBjWHGIwIuS9g5gB9cwVPV1tTPz+PrBUdvQKUWUgBuS5MbdpPzNKJq9Q6qTB khnso6s0+CQ7oIR30El3vxSueS7T6wfIFp/PL1jwesJ2AmBWz84I5n8dTwchFrXD dDMsAgx1Ea2QFbXSRnsjdY8Mufkt31SIG4e5xSERoQ2STkfsDiqWWhEywI2hOc3x E8+QhXyjwKw6W6d4Nt5tg5sFB2+4CJ4jKxzyE5jZHSNrf8365dicz9YEGSPuDUs7 oZcErGlbP+ixR0G/C0R0FG3+8XnhUJBJDd6wGiivM/y2ajfvIoEtfL0vg5pia2Bh hCUI8+jXg6TDvrGfGYAghDXNzY02KHP/sfGcDtQzc2qFuhlikdfowhT8+djJrVtm YNQeKie9XE8wYVUiuQE0ZRUFv2bY02Ur2WMzF+v7np+XcM5SMhGAWsV5xOQBv+6W msfhw6XNx2U5lGDtmI+r9l8dJgLGYIXKTJSfSGn1t8cLBbu/4OmpsP9UkWk= -----END CERTIFICATE----- mosquito-aiormq-612e0d0/tests/certs/client.key000066400000000000000000000062471527161700100214660ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJJgIBAAKCAgEAwaOK16e3rvrfO0b458LoLFEhjD0UQ8oPLYAonpIaNQb8DkLH Fd7JJdecFP4Iu2oB8SVp4ugbIYmCgpkrHUAucXutnuv+D7slMiWP3vDLpTYR2Nr/ Lo9QCmGfASjKveDyva8luVUVnLs8Io396ogVK3oPbAaJ+q9dK5SwX8fyxBUWV/Dl NzgbBOBINAvO8fOAzqE+CIqxsZPpzOZN+ljolMGbvfDPrXeZ6W4NMXcDOyzbovjM Lt6sL8zh395lPJKUpk+ZDmpd3GiucNX/8tS5Eu8bp8IuaAD9z0xYlFGkKcyokv7q mNKaoPSxm/nVqgTzGs3PmWPFq++/wAhdJvJZ7cW8GAnOqkyULRiBvaIrx1JcOuS8 ehQ9tHxKLW5C9VibVsZA2EkSuuCeavyYyvBZKdtFvZFOtiOt2WkflXakK7yFoOj+ /XFF0qpp5aFycVQrnJQSDbzlpyTHIW0j9EuxpVfpNiOFBvCLVoCviq0Aia2Tapo1 A0WxZA7FSXmxMRD7i79oqhzlKoXTYAc6bzDLOqjxYrTnjv18SqF2HEDtRLT7uxhh utMQYCwsfea3tzNXPgs+ffPS9H3lWM4JaytE2mFPUMvfUQaHMk8KsUTU/xfhLN1m vcgzqqKcxRl+qmLZOqkpUv39X9KKOIgMnPDY5ymYVRCWnr1FxcwCfa8Npk0CAwEA AQKCAgBjq08644wrV9vxQf26JVul+/idm47DucyIKhA+VouAweCZYovg2PSGMu2W 7I8IEG+BdTWEYt4cLBBuMnK7sp51MSjTxTrXVAe4QRdFtIHNvv/+s/JnP8L+JPNY AGwiwhePxQhQ1dey/bjdPGL3BiaHY2NuwgrhasQ1O2pxUpTFkukWSNtiydE2eE8R 4wYZCbJCKUKp2OHPuoe8PMrkUkEc2G7WnI35BrfFLC1ESbLzEYrX3uISOfE9BWM5 /Nn1DKnQ1OW+QsefPI6Va8E7d3zvnv2IIu4KAICj4/MwHLm3/izCxM1x7e1Dbc/B rh3pTnTnVgpGNNG5R0VWjbeM5W+dizwRmur/KTa5irjAbooS/k7PXfK/6J8kEdb4 lcxIvCpmYee5Zpl0AjzyUuQn7cX56c/Hnv5S3YqKI3oGfn3yGLsb7sZLcgQCypgM iNmYwCk0ZXzjZWAJ1O0WOelK6Yv0zTpkWhRoMAVCwK3pBH7C+MiO3FsDx9PiIM9r 6w2CkR8ije0jUykxERTJOlZe2tUEl8bDzmBOM6nF9TLd6uqpKjGcy8RNXNcWl5e7 OfQKBP+O8HxoK8rxNZR/o4XqRhi2yriiJFdd5nL7zxRHNNSkLzVc1+tb9miXFBQw 5GX7zECIZ1U4kv0vg3SSGv5SBibDKCxsq5HaQBDyTO/NHn2U4QKCAQEA5XdVV7Sq YKON1gjYcezaH85SUq9gxs7Itv17uI5VV3TSD22yZ/HRIj0HbLAPZEGKkkDvLxp8 cCwpdKU0FP5I+SVi/2jJNBqzMDdqipWlySUWU3EVYgp77n04QXJJauGoFTEpe9RT RZ0BzJ+Q9tvjwg/2o9HnJowjLCWSf9EHD56YiNzydZhy+AoYKegiDmjbh8XjFc/M BJAo/Hchq2GGZujplPqp0nHWRN07x+j/DGsMBWn192GxkGsoOoE+m3OHulODJ5kn 3+w9hahheZL5mZ4hEBR61VTsBV3AscXeUiK75+58ecyZwHB453y2IhRyXdtRAUdn fIhJahlLtZfEtQKCAQEA2AelwFdwkE20AS6JeRyTHPE8YqY7HcQvki/zjcP/Dava ZjqNr96Sq3Qq3auHU+tOBhQWUF7JHon0RWfuhoUHTJN4XxfcT3JocOjSKKR82M45 lUJdz3NJ9JVticVgotllo3B7DmyqEms7hBJzpgp8OrsqpUyu7LQ1OxmWtDpilYxe XpCgJekns79h3wJhSBZhMBB+DprXl4hoOucr1qXH2HAgFkWc80Jb/CGicUKSxqEB BSzHXjQCsqoqcbf8ghL2Bm9W4apPoONK6+IMNomKR3r0NKbXhy969+USj9DG40Zk lsO8nQ3Orerd8tbCkQwxWlLkcayXpDkV01PvJqmyOQKB/1qHuiPgI1f9LvhChSJt T6E8xT3Z81R8QLPxTd6CSSk37agonzpjLR9U9Jjs3SWwtfr9o1/yEyYuRiy/AM1H hYLGPUiHDtp/rjJXqrECWWYCO8yv0L/dYwe0X31ymYSRgr7ZpoQ0QKY2S39vdMHv /uuRYL1BEvEiWL4SFLpYvXBsIcHdacr7WmCBmwbtjoIg3Hu0luMEGHm0Znc0iRQU ZfIz8fPU8SsVvnNs1SkJw5YipZt9Mo1m/ab8n+J1Gz45VlMsn5H/2rt9eMhCpjJQ yijROjod2lhQKM31LxDz/8Jn8bqPXIyxK/fAZ/LsQO8xIe3lmQ/oG+wF2PEDCdub BQKCAQB9GECVFo0qIrS/knEs3q0Zr1+mSFgnLnnVj0rbpslE42T+mZ1+X8ZS3lwM LM2afMGbp3ocZCbWNlBq+HoZD2NgpmyntCtxHfD4oPlBa66X5SNXGS01ea8zoGvj wZXp9zVx5Sp8+dOqAspd+klZtuylHcjeG3+Xteq1JGYuSzjXHIdw/xKdoVvKLGLC PqCSm9L/gC1ey69YIjcpFMA/9ZO584PBIeJ2wtB9OgTUzRYtSwJKOtnf5QJC72LQ oxfnQo+QvlxzJKojojq6SRWFZzPZnItZCdv4fjgY4F9VRDJHXXXWD9Zio6Iw97Y6 br4QPB1ADowWfzj4cc3/p7TukImRAoIBAQDJZxTKSeSQSrK6RM+dHKiP5SSR3oeW 7NwpcMUTQz1x4Dc4OHvmviQNpIT8SjiYcO2pa/dy2tSp+0lkd08iDvOl5BJ5YUm5 O8nJol81u8qHtpOW1UmsZBdSsuCBRQfNa8szGvjzF9gusG/rjfGHoPLHGpl2sQYB MtzK91q7UaDFb8fi86ruZQT0Q4k+SXjeMCW6/k11XoqZp8gfotToNgLzBxh50yye EVTw4MuSyhX17yJShD+SA1RG58GUTa/2L/vqUi1z3P0+zUFyxxQDNYoPJvXnvTx+ uLjqzkOmw31r+kbjWD5NAx80SC01mI2T/VeTj2HtYkX3KGxfJQKfYOet -----END RSA PRIVATE KEY----- mosquito-aiormq-612e0d0/tests/certs/client.pem000066400000000000000000000102241527161700100214450ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIF7TCCA9WgAwIBAgIBBDANBgkqhkiG9w0BAQ0FADCBgzELMAkGA1UEBhMCUlUx DzANBgNVBAgTBk1vc2NvdzEPMA0GA1UEBxMGTW9zY293MQ0wCwYDVQQKEwRIb21l MRAwDgYDVQQLEwdJVCBDcmV3MRAwDgYDVQQDEwdUZXN0IENBMR8wHQYJKoZIhvcN AQkBFhByb290QGV4YW1wbGUuY29tMCAXDTE4MTIxNTExMzcwMFoYDzIyMTcxMjE1 MTEzNzAwWjCBhTELMAkGA1UEBhMCUlUxDzANBgNVBAgTBk1vc2NvdzEPMA0GA1UE BxMGTW9zY293MQ0wCwYDVQQKEwRIb21lMRAwDgYDVQQLEwdJVCBDcmV3MRIwEAYD VQQDEwlsb2NhbGhvc3QxHzAdBgkqhkiG9w0BCQEWEGNsaWVudEBsb2NhbGhvc3Qw ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDBo4rXp7eu+t87Rvjnwugs USGMPRRDyg8tgCiekho1BvwOQscV3skl15wU/gi7agHxJWni6BshiYKCmSsdQC5x e62e6/4PuyUyJY/e8MulNhHY2v8uj1AKYZ8BKMq94PK9ryW5VRWcuzwijf3qiBUr eg9sBon6r10rlLBfx/LEFRZX8OU3OBsE4Eg0C87x84DOoT4IirGxk+nM5k36WOiU wZu98M+td5npbg0xdwM7LNui+Mwu3qwvzOHf3mU8kpSmT5kOal3caK5w1f/y1LkS 7xunwi5oAP3PTFiUUaQpzKiS/uqY0pqg9LGb+dWqBPMazc+ZY8Wr77/ACF0m8lnt xbwYCc6qTJQtGIG9oivHUlw65Lx6FD20fEotbkL1WJtWxkDYSRK64J5q/JjK8Fkp 20W9kU62I63ZaR+VdqQrvIWg6P79cUXSqmnloXJxVCuclBINvOWnJMchbSP0S7Gl V+k2I4UG8ItWgK+KrQCJrZNqmjUDRbFkDsVJebExEPuLv2iqHOUqhdNgBzpvMMs6 qPFitOeO/XxKoXYcQO1EtPu7GGG60xBgLCx95re3M1c+Cz5989L0feVYzglrK0Ta YU9Qy99RBocyTwqxRNT/F+Es3Wa9yDOqopzFGX6qYtk6qSlS/f1f0oo4iAyc8Njn KZhVEJaevUXFzAJ9rw2mTQIDAQABo2YwZDAMBgNVHRMBAf8EAjAAMB0GA1UdDgQW BBSfZdZQewepSjbTanxArYrWyx8q8jALBgNVHQ8EBAMCBLAwEQYJYIZIAYb4QgEB BAQDAgeAMBUGCWCGSAGG+EIBDQQIFgZjbGllbnQwDQYJKoZIhvcNAQENBQADggIB AJcOeq/XJHs10/ol/D2Wp7NEkj5pAmASfz6K4j3qTC1e3gcKiLh6efnWDxixUlPM VlVxsMOd7mGA1mIGCyMzFFUs8P6ANjjiYapzDFsjuYeH5NFYqmGtL9chU9aNy3fD e5eDbmgNHKw0jotE7ffJSjy4SLq3sX03ia+scuMGIrCfMoca9NH6d5AtNvaOTyYn qVVmbhusYLcx64lc5VdgAJiXJCjTZwhhoHfOASLMxnXzJEe/PCdA5JadRKMNA1iN 1EmMbo0JReRWYbA+zptlI7NoYCKJpCIWiMEnd33rw9ybYLPKQn3kQ5EVPV1YCCz3 ksl9RClSJ2PFR3hZWKcIsrdkzQ3UTzcBSHSvi+HMhEZBNCqDyq+d9Jjh03mzdorW NWdDNQwtboJL2KwTIjRIe3lptmA/34c26GlZehw4vbRxJZWRqNwKWVN26gdK4vhZ 9gNRITZ3/cBf9e5dpKkxC0JjAJNZdcEocci4wO5OopNPMSBn1RCZI6HU1gd0S/Mj 36E+Wkt0340FZ71BukWtW3NAzcGLN37f6ntXs3VmtojFq3N4S1y15cTe8aRVboIA R+ga0r9AgA0zM9K0hrD6WfnZfAY346x4uP8SPVLI2/Nw8mxsmzWbRz4RokYtMB22 66JhlcKTdMPqgarlAa56Z5dld1012KyPBDUe7APsM2PP -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF+DCCA+CgAwIBAgIBATANBgkqhkiG9w0BAQ0FADCBgzELMAkGA1UEBhMCUlUx DzANBgNVBAgTBk1vc2NvdzEPMA0GA1UEBxMGTW9zY293MQ0wCwYDVQQKEwRIb21l MRAwDgYDVQQLEwdJVCBDcmV3MRAwDgYDVQQDEwdUZXN0IENBMR8wHQYJKoZIhvcN AQkBFhByb290QGV4YW1wbGUuY29tMCAXDTE4MTIxNTExMzEwMFoYDzIyMTgxMjE1 MTEzMTAwWjCBgzELMAkGA1UEBhMCUlUxDzANBgNVBAgTBk1vc2NvdzEPMA0GA1UE BxMGTW9zY293MQ0wCwYDVQQKEwRIb21lMRAwDgYDVQQLEwdJVCBDcmV3MRAwDgYD VQQDEwdUZXN0IENBMR8wHQYJKoZIhvcNAQkBFhByb290QGV4YW1wbGUuY29tMIIC IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2PtC5aCUO3dj1H6rK3pXwUFY msIMB6uI3cKqx4U3thyL3orTAYu51Ax/nG8iVi9X3CY0v0Bfwrq004oqCFuyygl0 yNRKomx9prpunCRv+vW6ojpif+iMOJmyGKQ8vhMSCbUgbk2Z53U1FKYWybPk9bXA fpI1KdyT2iVB0wDKKLkbLTtMuSOiFDCYZK5+yVBhbxIBQtoldhejbgHh0z7r78Bz v5vRwTyL73xHJydj+7yxJp4BgcptGYMJO6pb+c8vbLBdQy38i1vAAQa1XQzh8jUU KMXsO6LssMAdVMpA53uscQNz8j7g+cGwnWPe2t1f8I81tbI/oZ3MAf42zSAfbvde x9LbaXmmYAcifqdkkHaaaTKr8jVZyxK/CKMUzsL7JckDxXMAQgoKofWespyi2cqF /XISEnFaqOFh5brxwZmIKy2/GAqpyIv2BPWefgEhw1+d6GZMysPiSZKOdGUtoqlW 1Ni55z/gKLYNcTRyah7cGP6tGasSFjZAvMmgW6I+Q559pUXfsCSI+MGzGgeBNiPX 460j4qfyM/2kvR2vIjtY5choV2utDk3XMr5jSPatFKPX0K136TTMqEgBHL3SQ228 apR18MrNYwnlOSxXsr/85s/Hf7dtk7OPRIxJPiewUnUK2UrLQ8eLYdSMjJgJxfmc 2a2DQwq/0MwQomcJbtECAwEAAaNzMHEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E FgQUcylnYt/dChvaK5Ndc/Ko25dXrfswCwYDVR0PBAQDAgEGMBEGCWCGSAGG+EIB AQQEAwIABzAfBglghkgBhvhCAQ0EEhYQdGVzdCBjZXJ0aWZpY2F0ZTANBgkqhkiG 9w0BAQ0FAAOCAgEATHN9WJedxwW3bDr5yO1BgOVGYaGpbg2DheHnL61smQwimfZm T8z/1mn9iKBJd++lh/lfCTINlX4N3ViCTgDx+dEilQ80RQlbpi5fcrsU0xbuBHBX Pn/I61CuGebfFMQLD8qazkuZ7SDZMA5LdmPL7FnIJpHdl7DnnghyLHuakLn/7Qlu UDh8WBjWHGIwIuS9g5gB9cwVPV1tTPz+PrBUdvQKUWUgBuS5MbdpPzNKJq9Q6qTB khnso6s0+CQ7oIR30El3vxSueS7T6wfIFp/PL1jwesJ2AmBWz84I5n8dTwchFrXD dDMsAgx1Ea2QFbXSRnsjdY8Mufkt31SIG4e5xSERoQ2STkfsDiqWWhEywI2hOc3x E8+QhXyjwKw6W6d4Nt5tg5sFB2+4CJ4jKxzyE5jZHSNrf8365dicz9YEGSPuDUs7 oZcErGlbP+ixR0G/C0R0FG3+8XnhUJBJDd6wGiivM/y2ajfvIoEtfL0vg5pia2Bh hCUI8+jXg6TDvrGfGYAghDXNzY02KHP/sfGcDtQzc2qFuhlikdfowhT8+djJrVtm YNQeKie9XE8wYVUiuQE0ZRUFv2bY02Ur2WMzF+v7np+XcM5SMhGAWsV5xOQBv+6W msfhw6XNx2U5lGDtmI+r9l8dJgLGYIXKTJSfSGn1t8cLBbu/4OmpsP9UkWk= -----END CERTIFICATE----- mosquito-aiormq-612e0d0/tests/certs/rabbitmq.conf000066400000000000000000000016741527161700100221450ustar00rootroot00000000000000# Configuration for the aiormq test broker image. # # The official image since 3.9 ignores the RABBITMQ_SSL_* environment # variables, therefore the TLS listener needs this file. listeners.tcp.default = 5672 listeners.ssl.default = 5671 ssl_options.cacertfile = /certs/ca.pem ssl_options.certfile = /certs/server.pem ssl_options.keyfile = /certs/server.key # The test suite connects with and without a client certificate. ssl_options.verify = verify_peer ssl_options.fail_if_no_peer_cert = false management.ssl.port = 15671 management.ssl.cacertfile = /certs/ca.pem management.ssl.certfile = /certs/server.pem management.ssl.keyfile = /certs/server.key # The test suite and the README examples declare transient non-exclusive # queues. RabbitMQ 4 forbids them by default. deprecated_features.permit.transient_nonexcl_queues = true # The test suite publishes messages larger than the RabbitMQ 4 default # limit of 16 MiB. max_message_size = 134217728 mosquito-aiormq-612e0d0/tests/certs/server.key000066400000000000000000000062531527161700100215130ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKAIBAAKCAgEA0HIWng2ZA2aB+rDV4radi/jVwyVlGZXA5Fm1L3LYc7/+QkMH R1SGPdIL34xAJJ3UzTJT5ZF3xgmH/iyorDiOnR3z1dJfq8GsmWksf0/20mi6Kf1d 0rFeateyWY+P+lMbvjXOtE7xSa+/BAu3rkRM0n6YbZ8gHNEv+E31L14knDHxFzwq YeJKCvnA6Za1CeweGvqI5whFIFUYjUupi02/+jZTp97N00wMrQDXDYiiz+zcryqH SHyORJw/fgPp5/gNv/zEt+WrYgMmuwVlScK/8wtkLc5HmNRRuW0eTRXag9xNMVMq mSt1/8Ttkot5/8z1jklUVV6JqgEF6jjBkB4nPna9xMXJcf9PVIAuh4a74XmxRGSV aF6u1SvyZ7DWRhbATbn8lW7M1Xglrd6tK/IbjttOWnIt1kLMlbdxdQg/yZ58KdvX E6BCHJgi2KowbIQsPVTv33BTd3eqfwqfkOToMXlz4jnK1mgoTkdye0hOHoZ91kBC e5TIhK8fMRAPRYtTUW97SxudHaxVent8INeqz5MPHHZdofaBVCpihXHPfnphG5Rc vyjGa8wGHaImEK81efgm8E3jF7kW2Fn1mV1IRNu/QvyDbzgA6LRHCXV1njh1aozG 7LDinp5EzVNTASAI+Yn0KdU4V3/fuayXBFq/407gz2OQrG3lB9J/eOIll+sCAwEA AQKCAgAExVe3LmB+L25yKnH6yms4tO1Plh+GQmMz1snK2DoUDCTpp1cXTtvztkcH StJ9BA/G0owRCQ9QvQ8bxjHmHzVEa1cVYcdGyxwENuAJ2e6wSi1YoK/xDpY2o9E1 M4/8DsLny5t7jQMAyMD6erothuqrNrKOb8HwZulOKZqfBuyXlp0KBxqBOwiuz6CW uBhUrc7Sl0Fi6FGMt+Xj9gNfaNwoAe5QPU1AtNDldMt3R9VSJP24FKUcB53J/DmH zNchtA+8gTCPdPZDPAc66Ji043w5N92HHt2Mpe9o6xJyeTmTIwuxQVIMR25f+EXn wMF+FVbZdtwzSAKmnXdhMQNdJROI0+ONdawW8vQyuHaRJdZpqtCwqGqr9jp9Mpap b6y6AbxC6RI0a945egBjw/ic36EGIJ3EkW+PupvUTWmRFm2RgDA28De/eP92OOMY eUqa/3WhKMCEWrEsSq32gDIA+DdN77Az2uStt020BIGP1bfjlLVQMdUqGWpQ/5QR EIzXLYUXU2Quo6Y1LZW6iTdPoiAM4WeWHkNho4AC7RgXPyqMPFB2GDGA2rneBXU9 roqgh08Uldb00ZP9M9dgxRpt1YpL0B7VFTwv+zzvA7StopcJrhhzzDISzEomxPYH I/PbPrGb3ffai2WJPECeXrF2gIHoN8A0YAgrxAhFD+ElUsgRoQKCAQEA6oc32D86 JkHZjcvoHnmawVS5O3lxkE+2aQ1mpP6d2Oo4CccSUSw/wQnqJFD0NDkNHH8D53H4 VReE8TlowgSvM1NUPaK0y5XGu4J/ySWMYuPZLxIOKtJgoCOMj5/3gZfFU8B4j3aV NhyoLZJQ0cEWoY40KI6finY3FiCo8ipDd60/TuTySyfBwTlXjlMTIYRi3QXavbSB 7P3TaX+dEurJhwzRTnH21uP6/Xl9yKQzlKkCaJRzXOhg4F+UrurmOmD716aw6AWV 46ByO+D0amDkpT7IgMDSznqjh2sxNXoDL7fqp/1Ud/AjSOyVX9JsLJ7RXHvPwt0u jn0ejHy4WpPCfwKCAQEA44eOxFDEo/NAzqq13cCncwKFgqcp2j3EdyR85I+wsi9n YYMU2kY+S5x8q0bpMZ1B+jhEPu+NAOW4f5XUS+vtEBJ0mwP0NRxtA1A+MGOSouHi t6vLQzISxWScx175Q91d+7FxVDjYEWBqVP+CpmJ72H7Wzb4cdBBa7XaSQGKqSuYY 3yKhNKp3Qq2GF9pm5U7fJpiyunPk1gg1eLEDzdyd7x1EBHMjE0wgXgkEPR+36Mfg iNLjQAfknTmMoZ8FD8Q8/4xrKBJ2cOSEA7cXN1blwUVUHMVrZq50wWFxdAjqY4ql 0v5BWomh0ZlD0eqeq6uHFSNw6Ph5vfL9C5t20GCclQKCAQEArlj3Wvsl72rkoFUF qiIcubySN3SAyBd6M36S3/WowqjcH+it5UpP2uHT/ktwP6Jp7NU/wb8oLZnearWS +ykgVbeM2IUsgmxF4P+Sn6YaRym7OxLhFVRwIJxM0jjJdr2tJCXhekVdh2ymWbp7 +nLgsBlXDQ956yUWrox5DA3/OejBN5VbyiM0FsDaJiP8BN614DmJ851NOTE5CSSl UHradltA/mAacIXrAKRgrdfjwJAkCjrRyC+4VRS5I4/ct2mBzz9MJDCCzUVpproE +VAuqemShKTUEkt5ZiJ54pdh5weCmn/pW4BZusyl/yYe5MzsNySTvvlOsv6wxx+w rSVLYQKCAQATVZSTKA3dpLEQHr9/jXxtMHyp4oyS6AbG3Qnj3jX0nkSZq6rc9XUb tbt+TnNIbQWLPrbF5lNEDUFFTjUREoY9hGP2PDrHPJgi3PG76Oov/yPl2apXFm0z 6t3Lr01dL/VpiuWHc6EgsOG4QVIX02yUtAqKxynhzvX7EcVRxVCVNsJMS8QJFqc1 uksXwc5WlAIwZG9jmq+KZH4uuFQLbUDabdE205XacPCbLQb4LrbRCBMTbWA0M7eA iMBjh4DFmzZXvNXqPM9lvnVdX3SQlkjFyJ9iJoB+5Do1qJMcehl4xfJbYJGrIODo T67MqrQ7AENlT3KryVmHA5vvHZHWGS+VAoIBAElfBlcSxnIUNzJagEdCO9nFj0bl 5fAwjpj8+Hahww2bUhARbcTXvr0Tu7WA6UFwpKQpTtc5BxDtNYD3Hr8wbjnuK/f2 wXUrno3SsCr+e4sAxrukk4Zs7w4IoQLRgrO1scDVr2pgSZL2Yy63x/iXkbcOCzsg MJZ9Gn9ia4tS1Fhx5tJNz7TAyLEf8KTd5VJQ1ayVxhG0SZOxgj1Bldw4MA394oh/ OOs2KPSHH+QRFq29FUimQicFbXzWoME9iLcwxHlqmhMH6bk02TrG1rkJE0sSrFFc oOj0qkE6E+VnJpMq52zsYSHG3xTL8iLDd2WJ+QUlDFFacAzfwO3HtYS8+8A= -----END RSA PRIVATE KEY----- mosquito-aiormq-612e0d0/tests/certs/server.pem000066400000000000000000000103011527161700100214710ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIGDjCCA/agAwIBAgIBBTANBgkqhkiG9w0BAQ0FADCBgzELMAkGA1UEBhMCUlUx DzANBgNVBAgTBk1vc2NvdzEPMA0GA1UEBxMGTW9zY293MQ0wCwYDVQQKEwRIb21l MRAwDgYDVQQLEwdJVCBDcmV3MRAwDgYDVQQDEwdUZXN0IENBMR8wHQYJKoZIhvcN AQkBFhByb290QGV4YW1wbGUuY29tMCAXDTE4MTIxNTExMzMwMFoYDzIyMTcxMjE1 MTEzMzAwWjCBhTELMAkGA1UEBhMCUlUxDzANBgNVBAgTBk1vc2NvdzEPMA0GA1UE BxMGTW9zY293MQ0wCwYDVQQKEwRIb21lMRAwDgYDVQQLEwdJVCBDcmV3MRIwEAYD VQQDEwlsb2NhbGhvc3QxHzAdBgkqhkiG9w0BCQEWEHJvb3RAZXhhbXBsZS5jb20w ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDQchaeDZkDZoH6sNXitp2L +NXDJWUZlcDkWbUvcthzv/5CQwdHVIY90gvfjEAkndTNMlPlkXfGCYf+LKisOI6d HfPV0l+rwayZaSx/T/bSaLop/V3SsV5q17JZj4/6Uxu+Nc60TvFJr78EC7euREzS fphtnyAc0S/4TfUvXiScMfEXPCph4koK+cDplrUJ7B4a+ojnCEUgVRiNS6mLTb/6 NlOn3s3TTAytANcNiKLP7NyvKodIfI5EnD9+A+nn+A2//MS35atiAya7BWVJwr/z C2QtzkeY1FG5bR5NFdqD3E0xUyqZK3X/xO2Si3n/zPWOSVRVXomqAQXqOMGQHic+ dr3Exclx/09UgC6HhrvhebFEZJVoXq7VK/JnsNZGFsBNufyVbszVeCWt3q0r8huO 205aci3WQsyVt3F1CD/Jnnwp29cToEIcmCLYqjBshCw9VO/fcFN3d6p/Cp+Q5Ogx eXPiOcrWaChOR3J7SE4ehn3WQEJ7lMiErx8xEA9Fi1NRb3tLG50drFV6e3wg16rP kw8cdl2h9oFUKmKFcc9+emEblFy/KMZrzAYdoiYQrzV5+CbwTeMXuRbYWfWZXUhE 279C/INvOADotEcJdXWeOHVqjMbssOKenkTNU1MBIAj5ifQp1ThXf9+5rJcEWr/j TuDPY5CsbeUH0n944iWX6wIDAQABo4GGMIGDMAwGA1UdEwEB/wQCMAAwHQYDVR0O BBYEFIxbKKeY60LCPZxa/3cRQvvCnjCdMAsGA1UdDwQEAwIF4DAaBgNVHREEEzAR hwR/AAABgglsb2NhbGhvc3QwEQYJYIZIAYb4QgEBBAQDAgZAMBgGCWCGSAGG+EIB DQQLFglsb2NhbGhvc3QwDQYJKoZIhvcNAQENBQADggIBAEmG6ijYL0NUfkH3BTd+ tSmLzetOOmec7YrIVTA0VNUgXaQLiQ814qr1Bc14Q0f5jDMb+Nq+t0iGwGHQpSwr 2es5jU5hAggPfvnT2htT4aSz6qvv9LVurdaeSuP5kLRhKNmHbfts75Mpvaxk0NH1 n5ScQId/7Heg0s0nvZn8MYoLDSI0WoZLewRNia2igmcB2r5/YYWDzjakt7zmQzsH P4jjvdSrmJgcpe8F3tZLPcNk/3ib330kt9GM8BzPlIMNzpXrbslWWcurzIrC25dJ 5gYyOxkjCZ9dgvjMFov8tKgszSVVj7jI4AbDMlPdCNhY+h2VwYP67QJgX2lSkGVm WkgTv2aJ5/ElV7CqcEl4/tT4ncfjIKDy6F9rUXcR6qVF4UHXFpilgKo+tJMpRlxq f6Qbzkda83tU3LaGcc1PeY/e13eEH4I23ms4ffieUGbhx4OJ1RYAukoiapQWbCJl 9zLIkRHXlmWspMvJHsTvESANx1ImCK4HySqbheaq8K1N1dH+7uHRbSF+QK14yKTj u7a3cw3tqlg7SfBSywBEbkv83KiBLGWcTvo8yS9xcaIRoqnqakrgRe5pAYRnu/zh IW4GrF45UB6Meqc9Cb+6yCHp4OmkfJZQoEdqhaVv8JP+D+Q4tF0UaLmqP2ybtk6L MvekAEBEOasl6ZnGM/QQBN1o -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIF+DCCA+CgAwIBAgIBATANBgkqhkiG9w0BAQ0FADCBgzELMAkGA1UEBhMCUlUx DzANBgNVBAgTBk1vc2NvdzEPMA0GA1UEBxMGTW9zY293MQ0wCwYDVQQKEwRIb21l MRAwDgYDVQQLEwdJVCBDcmV3MRAwDgYDVQQDEwdUZXN0IENBMR8wHQYJKoZIhvcN AQkBFhByb290QGV4YW1wbGUuY29tMCAXDTE4MTIxNTExMzEwMFoYDzIyMTgxMjE1 MTEzMTAwWjCBgzELMAkGA1UEBhMCUlUxDzANBgNVBAgTBk1vc2NvdzEPMA0GA1UE BxMGTW9zY293MQ0wCwYDVQQKEwRIb21lMRAwDgYDVQQLEwdJVCBDcmV3MRAwDgYD VQQDEwdUZXN0IENBMR8wHQYJKoZIhvcNAQkBFhByb290QGV4YW1wbGUuY29tMIIC IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2PtC5aCUO3dj1H6rK3pXwUFY msIMB6uI3cKqx4U3thyL3orTAYu51Ax/nG8iVi9X3CY0v0Bfwrq004oqCFuyygl0 yNRKomx9prpunCRv+vW6ojpif+iMOJmyGKQ8vhMSCbUgbk2Z53U1FKYWybPk9bXA fpI1KdyT2iVB0wDKKLkbLTtMuSOiFDCYZK5+yVBhbxIBQtoldhejbgHh0z7r78Bz v5vRwTyL73xHJydj+7yxJp4BgcptGYMJO6pb+c8vbLBdQy38i1vAAQa1XQzh8jUU KMXsO6LssMAdVMpA53uscQNz8j7g+cGwnWPe2t1f8I81tbI/oZ3MAf42zSAfbvde x9LbaXmmYAcifqdkkHaaaTKr8jVZyxK/CKMUzsL7JckDxXMAQgoKofWespyi2cqF /XISEnFaqOFh5brxwZmIKy2/GAqpyIv2BPWefgEhw1+d6GZMysPiSZKOdGUtoqlW 1Ni55z/gKLYNcTRyah7cGP6tGasSFjZAvMmgW6I+Q559pUXfsCSI+MGzGgeBNiPX 460j4qfyM/2kvR2vIjtY5choV2utDk3XMr5jSPatFKPX0K136TTMqEgBHL3SQ228 apR18MrNYwnlOSxXsr/85s/Hf7dtk7OPRIxJPiewUnUK2UrLQ8eLYdSMjJgJxfmc 2a2DQwq/0MwQomcJbtECAwEAAaNzMHEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E FgQUcylnYt/dChvaK5Ndc/Ko25dXrfswCwYDVR0PBAQDAgEGMBEGCWCGSAGG+EIB AQQEAwIABzAfBglghkgBhvhCAQ0EEhYQdGVzdCBjZXJ0aWZpY2F0ZTANBgkqhkiG 9w0BAQ0FAAOCAgEATHN9WJedxwW3bDr5yO1BgOVGYaGpbg2DheHnL61smQwimfZm T8z/1mn9iKBJd++lh/lfCTINlX4N3ViCTgDx+dEilQ80RQlbpi5fcrsU0xbuBHBX Pn/I61CuGebfFMQLD8qazkuZ7SDZMA5LdmPL7FnIJpHdl7DnnghyLHuakLn/7Qlu UDh8WBjWHGIwIuS9g5gB9cwVPV1tTPz+PrBUdvQKUWUgBuS5MbdpPzNKJq9Q6qTB khnso6s0+CQ7oIR30El3vxSueS7T6wfIFp/PL1jwesJ2AmBWz84I5n8dTwchFrXD dDMsAgx1Ea2QFbXSRnsjdY8Mufkt31SIG4e5xSERoQ2STkfsDiqWWhEywI2hOc3x E8+QhXyjwKw6W6d4Nt5tg5sFB2+4CJ4jKxzyE5jZHSNrf8365dicz9YEGSPuDUs7 oZcErGlbP+ixR0G/C0R0FG3+8XnhUJBJDd6wGiivM/y2ajfvIoEtfL0vg5pia2Bh hCUI8+jXg6TDvrGfGYAghDXNzY02KHP/sfGcDtQzc2qFuhlikdfowhT8+djJrVtm YNQeKie9XE8wYVUiuQE0ZRUFv2bY02Ur2WMzF+v7np+XcM5SMhGAWsV5xOQBv+6W msfhw6XNx2U5lGDtmI+r9l8dJgLGYIXKTJSfSGn1t8cLBbu/4OmpsP9UkWk= -----END CERTIFICATE----- mosquito-aiormq-612e0d0/tests/conftest.py000066400000000000000000000072211527161700100205460ustar00rootroot00000000000000import asyncio import gc import logging import os import tracemalloc import pamqp import pytest from aiomisc_pytest import TCPProxy from yarl import URL from aiormq import Connection def cert_path(*args): return os.path.join( os.path.abspath(os.path.dirname(__file__)), "certs", *args, ) amqp_url_ids = ["amqp", "amqp-named", "amqps", "amqps-client"] @pytest.fixture(params=amqp_url_ids, ids=amqp_url_ids) async def amqp_url(request, amqp_direct_url, rabbitmq_container): urls = { "amqp": amqp_direct_url, "amqp-named": amqp_direct_url.update_query(name="pytest"), "amqps": amqp_direct_url.with_scheme("amqps").with_port( rabbitmq_container.ports["5671/tcp"], ).with_query( {"cafile": cert_path("ca.pem"), "no_verify_ssl": 1}, ), "amqps-client": amqp_direct_url.with_scheme("amqps").with_port( rabbitmq_container.ports["5671/tcp"], ).with_query( { "cafile": cert_path("ca.pem"), "keyfile": cert_path("client.key"), "certfile": cert_path("client.pem"), "no_verify_ssl": 1, }, ), } return urls[request.param] @pytest.fixture async def amqp_connection(amqp_url, event_loop): connection = Connection(amqp_url, loop=event_loop) async with connection: yield connection channel_params = [ dict(channel_number=None, frame_buffer_size=10, publisher_confirms=True), dict(channel_number=None, frame_buffer_size=1, publisher_confirms=True), dict(channel_number=None, frame_buffer_size=10, publisher_confirms=False), dict(channel_number=None, frame_buffer_size=1, publisher_confirms=False), ] @pytest.fixture(params=channel_params) async def amqp_channel(request, amqp_connection): try: yield await amqp_connection.channel(**request.param) finally: await amqp_connection.close() @pytest.fixture(autouse=True) def memory_tracer(): tracemalloc.start() tracemalloc.clear_traces() filters = ( tracemalloc.Filter(True, pamqp.__file__), tracemalloc.Filter(True, asyncio.__file__), ) snapshot_before = tracemalloc.take_snapshot().filter_traces(filters) def format_stat(stats): items = [ "TOP STATS:", "%-90s %6s %6s %6s" % ("Traceback", "line", "size", "count"), ] for stat in stats: fname = stat.traceback[0].filename lineno = stat.traceback[0].lineno items.append( "%-90s %6s %6s %6s" % (fname, lineno, stat.size_diff, stat.count_diff), ) return "\n".join(items) try: yield gc.collect() snapshot_after = tracemalloc.take_snapshot().filter_traces(filters) top_stats = snapshot_after.compare_to( snapshot_before, "lineno", cumulative=True, ) if top_stats: logging.error(format_stat(top_stats)) raise AssertionError("Possible memory leak") finally: tracemalloc.stop() @pytest.fixture() async def proxy(tcp_proxy, localhost, amqp_url: URL): default_port = 5672 if amqp_url.scheme == "amqp" else 5671 port = amqp_url.port or default_port async with tcp_proxy(amqp_url.host, port) as proxy: yield proxy @pytest.fixture async def proxy_connection(proxy: TCPProxy, amqp_url: URL, event_loop): url = amqp_url.with_host( "localhost", ).with_port( proxy.proxy_port, ) connection = Connection(url, loop=event_loop) await connection.connect() try: yield connection finally: await connection.close() mosquito-aiormq-612e0d0/tests/docker_client.py000066400000000000000000000242241527161700100215300ustar00rootroot00000000000000import http.client import json import os import socket from dataclasses import dataclass from urllib.parse import urlparse DEFAULT_DOCKER_SOCKET = "/var/run/docker.sock" class DockerNotAvailableError(Exception): """Raised when Docker is not available.""" @dataclass class DockerHostInfo: """Parsed DOCKER_HOST connection info.""" is_tcp: bool # For Unix socket socket_path: str | None = None # For TCP host: str | None = None port: int | None = None @property def container_host(self) -> str: """Host where container ports are accessible.""" if self.is_tcp: return self.host or "127.0.0.1" return "127.0.0.1" def parse_docker_host(docker_host: str | None = None) -> DockerHostInfo: """Parse DOCKER_HOST environment variable or use default.""" if docker_host is None: docker_host = os.environ.get("DOCKER_HOST", "") if not docker_host: return DockerHostInfo( is_tcp=False, socket_path=DEFAULT_DOCKER_SOCKET, ) if docker_host.startswith("unix://"): return DockerHostInfo( is_tcp=False, socket_path=docker_host[7:], ) if docker_host.startswith("tcp://"): parsed = urlparse(docker_host) return DockerHostInfo( is_tcp=True, host=parsed.hostname or "127.0.0.1", port=parsed.port or 2375, ) # Assume it's a socket path return DockerHostInfo( is_tcp=False, socket_path=docker_host, ) def check_docker_available() -> DockerHostInfo: """ Check if Docker is available and raise DockerNotAvailableError if not. Returns DockerHostInfo on success for use by DockerClient. """ docker_host_env = os.environ.get("DOCKER_HOST", "") info = parse_docker_host(docker_host_env) if info.is_tcp: # TCP connection to remote Docker try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) sock.connect((info.host, info.port)) sock.close() return info except (socket.error, OSError) as e: raise DockerNotAvailableError( f"Cannot connect to Docker daemon at " f"tcp://{info.host}:{info.port}: {e}\n" "\n" "Docker is required to run the test suite.\n" "\n" "Possible solutions:\n" " 1. Check if Docker daemon is running on remote host\n" " 2. Verify DOCKER_HOST is correct\n" " 3. Check firewall/network connectivity\n", ) from e else: # Unix socket connection socket_path = info.socket_path assert socket_path is not None if not os.path.exists(socket_path): hints = [ "Docker is required to run the test suite.", "", "Possible solutions:", " 1. Start Docker daemon", " 2. Set DOCKER_HOST environment variable:", " export DOCKER_HOST=unix:///var/run/docker.sock", " export DOCKER_HOST=tcp://remote-host:2375", "", ] if docker_host_env: hints.insert( 0, f"DOCKER_HOST is set to '{docker_host_env}' " f"but socket '{socket_path}' does not exist.", ) else: hints.insert( 0, f"Docker socket not found at '{socket_path}' " "and DOCKER_HOST is not set.", ) raise DockerNotAvailableError("\n".join(hints)) try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(5) sock.connect(socket_path) sock.close() return info except (socket.error, OSError) as e: raise DockerNotAvailableError( f"Cannot connect to Docker daemon at '{socket_path}': {e}\n" "\n" "Docker is required to run the test suite.\n" "\n" "Possible solutions:\n" " 1. Start Docker daemon\n" " 2. Check Docker socket permissions\n" " 3. Add your user to the 'docker' group\n", ) from e @dataclass class ContainerInfo: id: str ports: dict[str, int] # {"5672/tcp": 32789, "5671/tcp": 32790} host: str # "127.0.0.1" for local, remote host for tcp:// class UnixHTTPConnection(http.client.HTTPConnection): """HTTP connection over Unix socket for Docker API.""" def __init__(self, socket_path: str): super().__init__("localhost") self.socket_path = socket_path def connect(self) -> None: self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(self.socket_path) class DockerClient: def __init__(self, docker_host_info: DockerHostInfo | None = None): if docker_host_info is None: docker_host_info = parse_docker_host() self.docker_host_info = docker_host_info def _get_connection(self) -> http.client.HTTPConnection: if self.docker_host_info.is_tcp: assert self.docker_host_info.host is not None return http.client.HTTPConnection( self.docker_host_info.host, self.docker_host_info.port, ) assert self.docker_host_info.socket_path is not None return UnixHTTPConnection(self.docker_host_info.socket_path) def _request( self, method: str, path: str, body: dict | None = None, ) -> dict | list | None: conn = self._get_connection() try: headers = {"Content-Type": "application/json"} if body else {} conn.request( method, path, body=json.dumps(body) if body else None, headers=headers, ) resp = conn.getresponse() data = resp.read() if resp.status >= 400: raise RuntimeError( f"Docker API error: {resp.status} {data.decode()}", ) return json.loads(data) if data else None finally: conn.close() def _request_stream(self, method: str, path: str) -> None: """Make a request that returns a stream (e.g., image pull).""" conn = self._get_connection() try: conn.request(method, path) resp = conn.getresponse() # Consume the stream while True: chunk = resp.read(4096) if not chunk: break if resp.status >= 400: raise RuntimeError(f"Docker API error: {resp.status}") finally: conn.close() def pull(self, image: str) -> None: """Pull an image from registry.""" # Parse image name and tag if ":" in image: from_image, tag = image.rsplit(":", 1) else: from_image, tag = image, "latest" path = f"/images/create?fromImage={from_image}&tag={tag}" self._request_stream("POST", path) def create( self, image: str, ports: list[str], environment: dict[str, str] | None = None, ) -> str: """Create a container and return its ID.""" # Build ExposedPorts config exposed_ports: dict[str, dict] = {} port_bindings: dict[str, list[dict[str, str]]] = {} for port in ports: exposed_ports[port] = {} # Bind to random host port port_bindings[port] = [{"HostIp": "0.0.0.0", "HostPort": ""}] # Build environment list env_list: list[str] = [] if environment: env_list = [f"{k}={v}" for k, v in environment.items()] body = { "Image": image, "ExposedPorts": exposed_ports, "HostConfig": { "PortBindings": port_bindings, "PublishAllPorts": False, }, } if env_list: body["Env"] = env_list result = self._request("POST", "/containers/create", body) assert isinstance(result, dict) return result["Id"] def start(self, container_id: str) -> None: """Start a container.""" self._request("POST", f"/containers/{container_id}/start") def inspect(self, container_id: str) -> dict: """Inspect a container.""" result = self._request("GET", f"/containers/{container_id}/json") assert isinstance(result, dict) return result def stop(self, container_id: str, timeout: int = 10) -> None: """Stop a container gracefully.""" self._request("POST", f"/containers/{container_id}/stop?t={timeout}") def kill(self, container_id: str) -> None: """Kill a container immediately.""" self._request("POST", f"/containers/{container_id}/kill") def remove(self, container_id: str) -> None: """Remove a container.""" self._request("DELETE", f"/containers/{container_id}") def _parse_ports(self, info: dict) -> dict[str, int]: """Parse port mappings from container inspect result.""" ports: dict[str, int] = {} network_settings = info.get("NetworkSettings", {}) port_bindings = network_settings.get("Ports", {}) for container_port, bindings in port_bindings.items(): if bindings: # Take the first binding's host port host_port = int(bindings[0]["HostPort"]) ports[container_port] = host_port return ports def run( self, image: str, ports: list[str], environment: dict[str, str] | None = None, ) -> ContainerInfo: """Pull, create, start, inspect - return ContainerInfo.""" self.pull(image) container_id = self.create(image, ports, environment=environment) self.start(container_id) info = self.inspect(container_id) return ContainerInfo( id=container_id, ports=self._parse_ports(info), host=self.docker_host_info.container_host, ) mosquito-aiormq-612e0d0/tests/test_channel.py000066400000000000000000000505401527161700100213720ustar00rootroot00000000000000import asyncio import uuid from os import urandom import aiomisc import pamqp.frame import pytest from aiomisc_pytest import TCPProxy import aiormq from aiormq.abc import DeliveredMessage async def test_simple(amqp_channel: aiormq.Channel): await amqp_channel.basic_qos(prefetch_count=1) assert amqp_channel.number queue = asyncio.Queue() deaclare_ok = await amqp_channel.queue_declare(auto_delete=True) consume_ok = await amqp_channel.basic_consume(deaclare_ok.queue, queue.put) await amqp_channel.basic_publish( b"foo", routing_key=deaclare_ok.queue, properties=aiormq.spec.Basic.Properties(message_id="123"), ) message: DeliveredMessage = await queue.get() assert message.body == b"foo" cancel_ok = await amqp_channel.basic_cancel(consume_ok.consumer_tag) assert cancel_ok.consumer_tag == consume_ok.consumer_tag assert cancel_ok.consumer_tag not in amqp_channel.consumers await amqp_channel.queue_delete(deaclare_ok.queue) deaclare_ok = await amqp_channel.queue_declare(auto_delete=True) await amqp_channel.basic_publish(b"foo bar", routing_key=deaclare_ok.queue) message = await amqp_channel.basic_get(deaclare_ok.queue, no_ack=True) assert message.body == b"foo bar" async def test_blank_body(amqp_channel: aiormq.Channel): await amqp_channel.basic_qos(prefetch_count=1) assert amqp_channel.number queue = asyncio.Queue() deaclare_ok = await amqp_channel.queue_declare(auto_delete=True) consume_ok = await amqp_channel.basic_consume(deaclare_ok.queue, queue.put) await amqp_channel.basic_publish( b"", routing_key=deaclare_ok.queue, properties=aiormq.spec.Basic.Properties(message_id="123"), ) message: DeliveredMessage = await queue.get() assert message.body == b"" cancel_ok = await amqp_channel.basic_cancel(consume_ok.consumer_tag) assert cancel_ok.consumer_tag == consume_ok.consumer_tag assert cancel_ok.consumer_tag not in amqp_channel.consumers await amqp_channel.queue_delete(deaclare_ok.queue) deaclare_ok = await amqp_channel.queue_declare(auto_delete=True) await amqp_channel.basic_publish(b"foo bar", routing_key=deaclare_ok.queue) message = await amqp_channel.basic_get(deaclare_ok.queue, no_ack=True) assert message.body == b"foo bar" async def test_bad_consumer(amqp_channel: aiormq.Channel, event_loop): channel: aiormq.Channel = amqp_channel await channel.basic_qos(prefetch_count=1) declare_ok = await channel.queue_declare() future = event_loop.create_future() await channel.basic_publish(b"urgent", routing_key=declare_ok.queue) consumer_tag = event_loop.create_future() async def bad_consumer(message): await channel.basic_cancel(await consumer_tag) future.set_result(message) raise Exception consume_ok = await channel.basic_consume( declare_ok.queue, bad_consumer, no_ack=False, ) consumer_tag.set_result(consume_ok.consumer_tag) message = await future await channel.basic_reject(message.delivery.delivery_tag, requeue=True) assert message.body == b"urgent" future = event_loop.create_future() await channel.basic_consume( declare_ok.queue, future.set_result, no_ack=True, ) message = await future assert message.body == b"urgent" assert message.delivery_tag is not None assert message.exchange is not None assert message.redelivered async def test_ack_nack_reject(amqp_channel: aiormq.Channel): channel: aiormq.Channel = amqp_channel await channel.basic_qos(prefetch_count=1) declare_ok = await channel.queue_declare(auto_delete=True) queue = asyncio.Queue() await channel.basic_consume(declare_ok.queue, queue.put, no_ack=False) await channel.basic_publish(b"rejected", routing_key=declare_ok.queue) message: DeliveredMessage = await queue.get() assert message.body == b"rejected" assert message.delivery_tag is not None assert message.exchange is not None assert not message.redelivered await channel.basic_reject(message.delivery.delivery_tag, requeue=False) await channel.basic_publish(b"nacked", routing_key=declare_ok.queue) message = await queue.get() assert message.body == b"nacked" await channel.basic_nack(message.delivery.delivery_tag, requeue=False) await channel.basic_publish(b"acked", routing_key=declare_ok.queue) message = await queue.get() assert message.body == b"acked" await channel.basic_ack(message.delivery.delivery_tag) async def test_confirm_multiple(amqp_channel: aiormq.Channel): """ RabbitMQ has been observed to send confirmations in a strange pattern when publishing simultaneously where only some messages are delivered to a queue. It sends acks like this 1 2 4 5(multiple, confirming also 3). This test is probably inconsequential without publisher_confirms This is a regression for https://github.com/mosquito/aiormq/issues/10 """ channel: aiormq.Channel = amqp_channel exchange = uuid.uuid4().hex await channel.exchange_declare(exchange, exchange_type="topic") try: declare_ok = await channel.queue_declare(exclusive=True) await channel.queue_bind( declare_ok.queue, exchange, routing_key="test.5", ) for i in range(10): messages = [ asyncio.ensure_future( channel.basic_publish( b"test", exchange=exchange, routing_key="test.{}".format(i), ), ) for i in range(10) ] _, pending = await asyncio.wait(messages, timeout=0.2) assert not pending, "not all publishes were completed (confirmed)" await asyncio.sleep(0.05) finally: await channel.exchange_delete(exchange) async def test_exclusive_queue_locked(amqp_connection): channel0 = await amqp_connection.channel() channel1 = await amqp_connection.channel() qname = str(uuid.uuid4()) await channel0.queue_declare(qname, exclusive=True) try: await channel0.basic_consume(qname, print, exclusive=True) with pytest.raises(aiormq.exceptions.ChannelLockedResource): await channel1.queue_declare(qname) await channel1.basic_consume(qname, print, exclusive=True) finally: await channel0.queue_delete(qname) async def test_remove_writer_when_closed(amqp_channel: aiormq.Channel): with pytest.raises(aiormq.exceptions.ChannelClosed): await amqp_channel.queue_declare( "amq.forbidden_queue_name", auto_delete=True, ) with pytest.raises(aiormq.exceptions.ChannelInvalidStateError): await amqp_channel.queue_delete("amq.forbidden_queue_name") async def test_proxy_connection(proxy_connection, proxy: TCPProxy): channel: aiormq.Channel = await proxy_connection.channel() await channel.queue_declare(auto_delete=True) async def test_declare_queue_timeout(proxy_connection, proxy: TCPProxy): for _ in range(3): channel: aiormq.Channel = await proxy_connection.channel() qname = str(uuid.uuid4()) with proxy.slowdown(read_delay=5, write_delay=0): with pytest.raises(asyncio.TimeoutError): await channel.queue_declare( qname, auto_delete=True, timeout=0.5, ) async def test_big_message(amqp_channel: aiormq.Channel): size = 20 * 1024 * 1024 message = urandom(size) await amqp_channel.basic_publish(message) async def test_routing_key_too_large(amqp_channel: aiormq.Channel): routing_key = "x" * 256 with pytest.raises(ValueError): await amqp_channel.basic_publish(b"foo bar", routing_key=routing_key) exchange = uuid.uuid4().hex await amqp_channel.exchange_declare(exchange, exchange_type="topic") with pytest.raises(ValueError): await amqp_channel.exchange_bind(exchange, exchange, routing_key) with pytest.raises(ValueError): await amqp_channel.exchange_unbind(exchange, exchange, routing_key) queue = await amqp_channel.queue_declare(exclusive=True) with pytest.raises(ValueError): await amqp_channel.queue_bind(queue.queue, exchange, routing_key) with pytest.raises(ValueError): await amqp_channel.queue_unbind(queue.queue, exchange, routing_key) await amqp_channel.exchange_delete(exchange) @aiomisc.timeout(20) async def test_channel_close_after_rpc_timeout(proxy_connection, proxy): # Regression test for issue #83. A timed out RPC call must not block # channel.close() and the connection must stay usable. channel = await proxy_connection.channel() with proxy.slowdown(1, 1): with pytest.raises(asyncio.TimeoutError): await channel.queue_declare(auto_delete=True, timeout=0.1) await asyncio.wait_for(channel.close(), timeout=5) assert not proxy_connection.is_closed channel = await proxy_connection.channel() await channel.queue_declare(auto_delete=True) await channel.close() @aiomisc.timeout(20) async def test_channel_close_while_rpc_pending(proxy_connection, proxy): # close() waits for the channel lock while an RPC call is pending. When # that call is cancelled the channel is closed once; a second # Channel.Close on the closed channel would be a protocol error. channel = await proxy_connection.channel() with proxy.slowdown(0.5, 0.5): declare = asyncio.ensure_future( channel.queue_declare(auto_delete=True), ) await asyncio.sleep(0.1) close = asyncio.ensure_future(channel.close()) await asyncio.sleep(0.1) declare.cancel() with pytest.raises(asyncio.CancelledError): await declare await asyncio.wait_for(close, timeout=10) # Give the broker time to react to the frames it received. await asyncio.sleep(0.5) assert not proxy_connection.is_closed channel = await proxy_connection.channel() await channel.queue_declare(auto_delete=True) await channel.close() @aiomisc.timeout(20) async def test_rpc_cancelled_during_put(amqp_connection: aiormq.Connection): # A cancel while the frame waits for the write queue must still close # the open channel on the broker with a Channel.Close handshake. channel = await amqp_connection.channel() class BlockingQueue: def __init__(self, queue: asyncio.Queue): self.queue = queue self.block = True async def put(self, item): if self.block: self.block = False await asyncio.Event().wait() # blocks until cancelled await self.queue.put(item) def __getattr__(self, name): return getattr(self.queue, name) channel.write_queue = BlockingQueue(channel.write_queue) # type: ignore declare = asyncio.ensure_future(channel.queue_declare(auto_delete=True)) await asyncio.sleep(0.1) declare.cancel() with pytest.raises(asyncio.CancelledError): await declare assert channel.number in amqp_connection.channels # The broker confirms the close and the number becomes free. while channel.number in amqp_connection.channels: await asyncio.sleep(0.05) other = await amqp_connection.channel(channel_number=channel.number) await other.queue_declare(auto_delete=True) await other.close() @aiomisc.timeout(20) async def test_consumer_cancel_callbacks(amqp_channel: aiormq.Channel): # The broker cancels the consumer when its queue is deleted. Every # callback in on_consumer_cancel_callbacks gets the Basic.Cancel frame. declare_ok = await amqp_channel.queue_declare(auto_delete=True) consume_ok = await amqp_channel.basic_consume( declare_ok.queue, lambda message: None, ) async_calls: asyncio.Queue = asyncio.Queue() sync_calls: list = [] async def on_cancel_async(frame: aiormq.spec.Basic.Cancel) -> None: await async_calls.put(frame) def on_cancel_sync(frame: aiormq.spec.Basic.Cancel) -> None: sync_calls.append(frame) def on_cancel_broken(frame: aiormq.spec.Basic.Cancel) -> None: raise RuntimeError("callback failure") amqp_channel.on_consumer_cancel_callbacks.update( {on_cancel_async, on_cancel_sync, on_cancel_broken}, ) await amqp_channel.queue_delete(declare_ok.queue) frame = await asyncio.wait_for(async_calls.get(), timeout=5) assert isinstance(frame, aiormq.spec.Basic.Cancel) assert frame.consumer_tag == consume_ok.consumer_tag while not sync_calls: await asyncio.sleep(0.01) assert sync_calls[0].consumer_tag == consume_ok.consumer_tag assert consume_ok.consumer_tag not in amqp_channel.consumers # The failing callback did not break the channel. assert not amqp_channel.is_closed await amqp_channel.queue_declare(auto_delete=True) @aiomisc.timeout(20) async def test_client_cancel_does_not_call_callbacks( amqp_channel: aiormq.Channel, ): # Basic.CancelOk answers a client basic_cancel() call. It is not a # broker-initiated cancel. declare_ok = await amqp_channel.queue_declare(auto_delete=True) consume_ok = await amqp_channel.basic_consume( declare_ok.queue, lambda message: None, ) calls: list = [] amqp_channel.on_consumer_cancel_callbacks.add(calls.append) await amqp_channel.basic_cancel(consume_ok.consumer_tag) await asyncio.sleep(0.1) assert calls == [] assert consume_ok.consumer_tag not in amqp_channel.consumers def written_frames(write_queue: asyncio.Queue) -> list: """Return the frames which wait in a channel write queue.""" frames = [] while not write_queue.empty(): payload = write_queue.get_nowait().payload while payload: consumed, _, frame = pamqp.frame.unmarshal(payload) payload = payload[consumed:] frames.append(frame) return frames @pytest.mark.parametrize("capability", [True, False]) @aiomisc.timeout(20) async def test_broker_cancel_answer_needs_capability( amqp_channel: aiormq.Channel, capability: bool, ): # A broker which advertises accept_consumer_cancel_ok holds the consumer # tag until the client answers Basic.Cancel with Basic.CancelOk. Brokers # without the capability close the channel on that answer. declare_ok = await amqp_channel.queue_declare(auto_delete=True) consume_ok = await amqp_channel.basic_consume( declare_ok.queue, lambda message: None, ) # The answer goes to a private queue, so it never reaches the broker # and the test result does not depend on the broker version. write_queue: asyncio.Queue = asyncio.Queue() pending_before_callback: list = [] done = asyncio.Event() def on_cancel(frame: aiormq.spec.Basic.Cancel) -> None: pending_before_callback.append(write_queue.qsize()) done.set() amqp_channel.on_consumer_cancel_callbacks.add(on_cancel) capabilities = amqp_channel.connection.server_capabilities original = capabilities.get("accept_consumer_cancel_ok") capabilities["accept_consumer_cancel_ok"] = capability original_write_queue = amqp_channel.write_queue amqp_channel.write_queue = write_queue try: # The broker always sets the nowait flag in its own Basic.Cancel. await amqp_channel.frames.put( (1, aiormq.spec.Basic.Cancel( consumer_tag=consume_ok.consumer_tag, nowait=True, )), ) await asyncio.wait_for(done.wait(), timeout=5) finally: amqp_channel.write_queue = original_write_queue if original is None: capabilities.pop("accept_consumer_cancel_ok", None) else: capabilities["accept_consumer_cancel_ok"] = original frames = written_frames(write_queue) if capability: assert len(frames) == 1 assert isinstance(frames[0], aiormq.spec.Basic.CancelOk) assert frames[0].consumer_tag == consume_ok.consumer_tag # The answer is queued before the callbacks run, therefore a # callback which consumes again with the same tag reaches the # broker later than the answer. assert pending_before_callback == [1] else: assert frames == [] assert pending_before_callback == [0] assert consume_ok.consumer_tag not in amqp_channel.consumers assert not amqp_channel.is_closed @aiomisc.timeout(20) async def test_broker_cancel_unknown_tag_is_not_answered( amqp_channel: aiormq.Channel, ): # An unknown consumer tag gets no answer. The broker holds no such # consumer, and brokers before 4.3.7 would close the channel. done = asyncio.Event() amqp_channel.on_consumer_cancel_callbacks.add(lambda frame: done.set()) write_queue: asyncio.Queue = asyncio.Queue() capabilities = amqp_channel.connection.server_capabilities original = capabilities.get("accept_consumer_cancel_ok") capabilities["accept_consumer_cancel_ok"] = True original_write_queue = amqp_channel.write_queue amqp_channel.write_queue = write_queue try: await amqp_channel.frames.put( (1, aiormq.spec.Basic.Cancel( consumer_tag="ctag-unknown", nowait=True, )), ) await asyncio.wait_for(done.wait(), timeout=5) finally: amqp_channel.write_queue = original_write_queue if original is None: capabilities.pop("accept_consumer_cancel_ok", None) else: capabilities["accept_consumer_cancel_ok"] = original assert written_frames(write_queue) == [] assert not amqp_channel.is_closed @aiomisc.timeout(60) async def test_consumer_timeout_allows_the_same_consumer_tag( amqp_direct_url, ): # RabbitMQ 4.3 and later cancel a timed out consumer instead of closing # the channel, and hold the consumer tag until the client answers. A # client which consumes again with the same tag before the answer gets # "NOT_ALLOWED - attempt to reuse consumer tag" and loses the whole # connection. async with aiormq.Connection(amqp_direct_url) as connection: if not connection.accept_consumer_cancel_ok: pytest.skip("Broker without accept_consumer_cancel_ok") channel = await connection.channel() queue = "consumer-timeout-%s" % uuid.uuid4().hex await channel.queue_declare( queue, durable=True, arguments={ "x-queue-type": "quorum", "x-consumer-timeout": 1000, }, ) redelivered: asyncio.Queue = asyncio.Queue() cancels: asyncio.Queue = asyncio.Queue() tasks: list = [] first_delivery = True async def consume(message: DeliveredMessage) -> None: nonlocal first_delivery if first_delivery: # The first delivery stays unacknowledged, so the broker # cancels the consumer after x-consumer-timeout. first_delivery = False return await message.channel.basic_ack(message.delivery_tag) await redelivered.put(message) async def reconsume(consumer_tag: str) -> None: await channel.basic_consume( queue, consume, consumer_tag=consumer_tag, ) def on_cancel(frame: aiormq.spec.Basic.Cancel) -> None: # A robust client consumes again with the same tag. The call # runs as a task, because an RPC inside the callback would wait # for the blocked channel reader. tasks.append(asyncio.ensure_future(reconsume(frame.consumer_tag))) cancels.put_nowait(frame) channel.on_consumer_cancel_callbacks.add(on_cancel) consume_ok = await channel.basic_consume(queue, consume) await channel.basic_publish(b"first", routing_key=queue) frame = await asyncio.wait_for(cancels.get(), timeout=30) assert frame.consumer_tag == consume_ok.consumer_tag await asyncio.wait_for(asyncio.gather(*tasks), timeout=30) # The broker redelivers the message to the restored consumer, which # proves the consumer tag and the connection still work. message = await asyncio.wait_for(redelivered.get(), timeout=30) assert message.body == b"first" assert consume_ok.consumer_tag in channel.consumers assert not channel.is_closed assert not connection.is_closed await channel.basic_cancel(consume_ok.consumer_tag) await channel.queue_delete(queue) mosquito-aiormq-612e0d0/tests/test_channel_futures.py000066400000000000000000000036471527161700100231550ustar00rootroot00000000000000import asyncio import pytest @pytest.mark.parametrize("method", ["basic_ack", "basic_nack", "basic_reject"]) @pytest.mark.parametrize("failure", ["cancel", "error"]) @pytest.mark.parametrize("wait", [False, True]) async def test_disposition_enqueue_failure( amqp_connection, monkeypatch, method, failure, wait, ): channel = await amqp_connection.channel() entered = asyncio.Event() frames = [] class Queue: async def put(self, frame): frames.append(frame) entered.set() if failure == "error": raise RuntimeError("enqueue failed") await asyncio.Event().wait() with monkeypatch.context() as patch: patch.setattr(channel, "write_queue", Queue()) task = asyncio.create_task(getattr(channel, method)(1, wait=wait)) await asyncio.wait_for(entered.wait(), 2) if failure == "cancel": task.cancel() error = asyncio.CancelledError if failure == "cancel" else RuntimeError with pytest.raises(error): await task drain = frames[0].drain_future if wait: assert drain is not None and drain.cancelled() await asyncio.sleep(0) assert drain not in channel._Base__future_store.futures assert drain not in amqp_connection._Base__future_store.futures else: assert drain is None await channel.close() @pytest.mark.parametrize("method", ["basic_ack", "basic_nack", "basic_reject"]) async def test_disposition_without_wait(amqp_connection, monkeypatch, method): channel = await amqp_connection.channel() frames = [] class Queue: async def put(self, frame): frames.append(frame) with monkeypatch.context() as patch: patch.setattr(channel, "write_queue", Queue()) await asyncio.wait_for(getattr(channel, method)(1, wait=False), 2) assert frames[0].drain_future is None await channel.close() mosquito-aiormq-612e0d0/tests/test_channel_number.py000066400000000000000000000077141527161700100227470ustar00rootroot00000000000000import asyncio from uuid import uuid4 import pytest import aiormq @pytest.mark.parametrize("owner", ["old", "new", "absent"]) async def test_local_channel_number_cleanup( event_loop: asyncio.AbstractEventLoop, owner: str, ) -> None: connection = aiormq.Connection( "amqp://guest:guest@localhost/", loop=event_loop, ) connection.connection_tune = aiormq.spec.Connection.Tune(frame_max=131072) channel = aiormq.Channel(connection, number=1, publisher_confirms=False) replacement = object() if owner == "old": connection.channels[1] = channel elif owner == "new": connection.channels[1] = replacement try: channel._close_locally() if owner == "new": assert connection.channels[1] is replacement else: assert 1 not in connection.channels finally: connection.channels.clear() await channel.close() await connection.close() async def test_close_ok_after_channel_number_reuse( amqp_connection: aiormq.Connection, monkeypatch: pytest.MonkeyPatch, ) -> None: channel = await amqp_connection.channel() received = asyncio.Event() resume = asyncio.Event() handled = asyncio.Event() original = channel._on_close_ok_frame async def delayed_close_ok(frame: aiormq.spec.Channel.CloseOk) -> None: received.set() await resume.wait() try: await original(frame) finally: handled.set() monkeypatch.setattr(channel, "_on_close_ok_frame", delayed_close_ok) closing = asyncio.create_task(channel.close()) try: await asyncio.wait_for(received.wait(), timeout=5) assert channel.number not in amqp_connection.channels other = await amqp_connection.channel( channel_number=channel.number, timeout=5, ) resume.set() await asyncio.wait_for(handled.wait(), timeout=5) await asyncio.wait_for(closing, timeout=5) assert amqp_connection.channels[channel.number] is other await other.queue_declare(auto_delete=True, timeout=5) await other.close() assert not amqp_connection.is_closed finally: resume.set() await asyncio.wait_for(closing, timeout=5) async def test_channel_number_reuse_after_broker_close( amqp_connection: aiormq.Connection, monkeypatch: pytest.MonkeyPatch, ) -> None: channel = await amqp_connection.channel() received = asyncio.Event() resume = asyncio.Event() original = channel._on_close_frame async def delayed_close(frame: aiormq.spec.Channel.Close) -> None: received.set() await resume.wait() await original(frame) monkeypatch.setattr(channel, "_on_close_frame", delayed_close) declaration = asyncio.create_task( channel.queue_declare(str(uuid4()), passive=True, timeout=5), ) try: await asyncio.wait_for(received.wait(), timeout=5) assert channel.number not in amqp_connection.channels finally: resume.set() with pytest.raises(aiormq.exceptions.ChannelNotFoundEntity): await asyncio.wait_for(declaration, timeout=5) other = await amqp_connection.channel( channel_number=channel.number, timeout=5, ) await other.queue_declare(auto_delete=True, timeout=5) await other.close() assert not amqp_connection.is_closed async def test_concurrent_channel_number_reuse( amqp_connection: aiormq.Connection, ) -> None: persistent = await amqp_connection.channel() async def churn() -> None: for _ in range(50): await asyncio.sleep(0) channel = await amqp_connection.channel(timeout=5) await channel.close(timeout=5) await asyncio.wait_for( asyncio.gather(*(churn() for _ in range(10))), timeout=20, ) assert amqp_connection.channels == {persistent.number: persistent} await persistent.queue_declare(auto_delete=True, timeout=5) assert not amqp_connection.is_closed mosquito-aiormq-612e0d0/tests/test_connection.py000066400000000000000000001106661527161700100221270ustar00rootroot00000000000000import asyncio import errno import inspect import itertools import logging import os import struct import ssl import sys import uuid import warnings from binascii import hexlify from typing import Any, Optional, Tuple import aiomisc import pytest from pamqp import exceptions as pamqp_exceptions from pamqp.commands import Basic from yarl import URL import aiormq from aiormq.abc import ChannelFrame, DeliveredMessage, SSLCerts from aiormq.auth import AuthBase, ExternalAuth, PlainAuth from aiormq.channel import Channel from aiormq.exceptions import AMQPConnectionError from aiormq.connection import ( Connection, SSLContextProvider, TransportFactory, parse_int, parse_timeout, parse_bool ) from .conftest import cert_path CERT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "certs")) async def test_simple(amqp_connection: aiormq.Connection): channel1 = await amqp_connection.channel() await channel1.basic_qos(prefetch_count=1) assert channel1.number channel2 = await amqp_connection.channel(11) assert channel2.number await channel1.close() await channel2.close() channel = await amqp_connection.channel() queue = asyncio.Queue() deaclare_ok = await channel.queue_declare(auto_delete=True) consume_ok = await channel.basic_consume(deaclare_ok.queue, queue.put) await channel.basic_publish(b"foo", routing_key=deaclare_ok.queue) message: DeliveredMessage = await queue.get() assert message.body == b"foo" with pytest.raises(aiormq.exceptions.PublishError) as e: await channel.basic_publish( b"bar", routing_key=deaclare_ok.queue + "foo", mandatory=True, ) message = e.value.message assert message.delivery.routing_key == deaclare_ok.queue + "foo" assert message.body == b"bar" assert "reply_text='NO_ROUTE'" in repr(e.value) assert f"routing_key={message.delivery.routing_key!r}" in repr(e.value) cancel_ok = await channel.basic_cancel(consume_ok.consumer_tag) assert cancel_ok.consumer_tag == consume_ok.consumer_tag await channel.queue_delete(deaclare_ok.queue) deaclare_ok = await channel.queue_declare(auto_delete=True) await channel.basic_publish(b"foo bar", routing_key=deaclare_ok.queue) message = await channel.basic_get(deaclare_ok.queue, no_ack=True) assert message.body == b"foo bar" await amqp_connection.close() with pytest.raises(RuntimeError): await channel.basic_get(deaclare_ok.queue) with pytest.raises(RuntimeError): await amqp_connection.channel() async def test_channel_reuse(amqp_connection: aiormq.Connection): for _ in range(10): channel = await amqp_connection.channel(channel_number=1) await channel.basic_qos(prefetch_count=1) await channel.close() del channel async def test_channel_closed_reuse(amqp_connection: aiormq.Connection): for _ in range(10): channel = await amqp_connection.channel(channel_number=1) with pytest.raises(aiormq.ChannelAccessRefused): await channel.exchange_declare("", passive=True, auto_delete=True) async def test_bad_channel(amqp_connection: aiormq.Connection): with pytest.raises(ValueError): await amqp_connection.channel(65536) with pytest.raises(ValueError): await amqp_connection.channel(-1) channel = await amqp_connection.channel(65535) with pytest.raises(aiormq.exceptions.ChannelNotFoundEntity): await channel.queue_bind(uuid.uuid4().hex, uuid.uuid4().hex) async def test_properties(amqp_connection): assert amqp_connection.connection_tune.channel_max > 0 assert amqp_connection.connection_tune.heartbeat > 1 assert amqp_connection.connection_tune.frame_max > 1024 await amqp_connection.close() async def test_open(amqp_connection): with pytest.raises(RuntimeError): await amqp_connection.connect() channel = await amqp_connection.channel() await channel.close() await amqp_connection.close() class _TcpTransportFactory(TransportFactory): async def create( self, url: URL, **kwargs: Any, ) -> Tuple[asyncio.StreamReader, asyncio.StreamWriter]: ssl_context_provider = kwargs.pop("ssl_context_provider") assert isinstance(ssl_context_provider, SSLContextProvider) loop = asyncio.get_event_loop() reader = asyncio.StreamReader(loop=loop) protocol = asyncio.StreamReaderProtocol(reader, loop=loop) if url.scheme == "amqps": ssl = await ssl_context_provider.get_context() else: ssl = None transport, _ = await loop.create_connection( lambda: protocol, url.host, url.port, ssl=ssl, ) writer = asyncio.StreamWriter(transport, protocol, reader, loop) return reader, writer async def test_open_with_transport_factory(amqp_url): async with aiormq.connect( amqp_url, transport_factory=_TcpTransportFactory(), ) as amqp_connection: channel = await amqp_connection.channel() await channel.close() async def test_connect_context_manager(amqp_url: URL): # aiormq.connect() prepares a connection. The context manager opens it # and closes it on exit. context = aiormq.connect(amqp_url) assert isinstance(context.connection, aiormq.Connection) assert not context.connection.is_opened async with context as connection: assert connection is context.connection assert connection.is_opened channel = await connection.channel() await channel.close() assert connection.is_closed async def test_connect_await(amqp_url: URL): # The pre-7.1 form works without a warning. with warnings.catch_warnings(): warnings.filterwarnings("error", message=".*aiormq.connect.*") connection = await aiormq.connect(amqp_url) assert isinstance(connection, aiormq.Connection) assert connection.is_opened await connection.close() async def test_connect_result_is_a_coroutine(amqp_url: URL, event_loop): # Callers that need a real coroutine, such as asyncio.create_task(), # keep working. connect() is still seen as a coroutine function: # inspect.markcoroutinefunction() needs Python 3.12, on 3.11 only the # asyncio check sees the legacy mark. if sys.version_info >= (3, 12): assert inspect.iscoroutinefunction(aiormq.connect) else: assert asyncio.iscoroutinefunction(aiormq.connect) context = aiormq.connect(amqp_url) assert asyncio.iscoroutine(context) connection = await event_loop.create_task(context) assert connection.is_opened await connection.close() # The Connection is created on first use, so a bad URL fails on # await, not on the connect() call, as before. context = aiormq.connect("not a url") with pytest.raises(Exception): await context async def test_connect_client_properties(amqp_url: URL): async with aiormq.connect( amqp_url, client_properties={"connection_name": "from-constructor"}, ) as connection: assert connection.is_opened # connect() still accepts client_properties and they win. connection = aiormq.Connection( amqp_url, client_properties={"connection_name": "from-constructor"}, ) await connection.connect({"connection_name": "from-connect"}) assert connection.is_opened await connection.close() async def test_channel_close(amqp_connection): channel = await amqp_connection.channel() assert channel.number in amqp_connection.channels await channel.close() assert channel.number not in amqp_connection.channels async def test_conncetion_reject(event_loop): with pytest.raises(ConnectionError): async with aiormq.connect( "amqp://guest:guest@127.0.0.1:59999/", loop=event_loop, ): pass connection = aiormq.Connection( "amqp://guest:guest@127.0.0.1:59999/", loop=event_loop, ) with pytest.raises(ConnectionError): await event_loop.create_task(connection.connect()) async def test_auth_base(amqp_connection): with pytest.raises(NotImplementedError): AuthBase(amqp_connection).marshal() async def test_auth_plain(amqp_connection, event_loop): auth = PlainAuth(amqp_connection).marshal() assert auth == PlainAuth(amqp_connection).marshal() auth_parts = auth.split("\x00") assert auth_parts == ["", "guest", "guest"] connection = aiormq.Connection( amqp_connection.url.with_user("foo").with_password("bar"), loop=event_loop, ) auth = PlainAuth(connection).marshal() auth_parts = auth.split("\x00") assert auth_parts == ["", "foo", "bar"] auth = PlainAuth(connection) auth.value = "boo" assert auth.marshal() == "boo" async def test_auth_external(amqp_direct_url): url = amqp_direct_url.with_scheme("amqps") url.update_query(auth="external") connection = aiormq.Connection auth = ExternalAuth(connection).marshal() auth = ExternalAuth(connection) auth.value = "" assert auth.marshal() == "" async def test_channel_closed(amqp_connection): for i in range(10): channel = await amqp_connection.channel() with pytest.raises(aiormq.exceptions.ChannelNotFoundEntity): await channel.basic_consume("foo", lambda x: None) channel = await amqp_connection.channel() with pytest.raises(aiormq.exceptions.ChannelNotFoundEntity): await channel.queue_declare( "foo_%s" % i, auto_delete=True, passive=True, ) await amqp_connection.close() async def test_timeout_default(amqp_direct_url, event_loop): connection = aiormq.Connection(amqp_direct_url, loop=event_loop) await connection.connect() assert connection.timeout == 60 await connection.close() async def test_timeout_1000(amqp_direct_url, event_loop): url = amqp_direct_url.update_query(timeout=1000) connection = aiormq.Connection(url, loop=event_loop) await connection.connect() assert connection.timeout await connection.close() async def test_heartbeat_0(amqp_direct_url, event_loop): url = amqp_direct_url.update_query(heartbeat=0) connection = aiormq.Connection(url, loop=event_loop) await connection.connect() assert connection.connection_tune.heartbeat == 0 await connection.close() async def test_heartbeat_default(amqp_direct_url, event_loop): connection = aiormq.Connection(amqp_direct_url, loop=event_loop) await connection.connect() assert connection.connection_tune.heartbeat == 60 await connection.close() async def test_heartbeat_above_range(amqp_direct_url, event_loop): url = amqp_direct_url.update_query(heartbeat=70000) connection = aiormq.Connection(url, loop=event_loop) await connection.connect() assert connection.connection_tune.heartbeat == 0 await connection.close() async def test_heartbeat_under_range(amqp_direct_url, event_loop): url = amqp_direct_url.update_query(heartbeat=-1) connection = aiormq.Connection(url, loop=event_loop) await connection.connect() assert connection.connection_tune.heartbeat == 0 await connection.close() async def test_heartbeat_not_int(amqp_direct_url, event_loop): url = amqp_direct_url.update_query(heartbeat="None") connection = aiormq.Connection(url, loop=event_loop) await connection.connect() assert connection.connection_tune.heartbeat == 0 await connection.close() async def test_bad_credentials(amqp_url: URL): with pytest.raises(aiormq.exceptions.ProbableAuthenticationError): async with aiormq.connect(amqp_url.with_password(uuid.uuid4().hex)): pass async def test_non_publisher_confirms(amqp_connection): amqp_connection.server_capabilities["publisher_confirms"] = False with pytest.raises(ValueError): await amqp_connection.channel(publisher_confirms=True) await amqp_connection.channel(publisher_confirms=False) @pytest.mark.slow async def test_no_free_channels(amqp_connection: aiormq.Connection): await asyncio.wait_for( asyncio.gather( *[ amqp_connection.channel(n + 1) for n in range(amqp_connection.connection_tune.channel_max) ], ), timeout=120, ) with pytest.raises(aiormq.exceptions.ConnectionNotAllowed): await asyncio.wait_for(amqp_connection.channel(), timeout=5) async def test_huge_message(amqp_connection: aiormq.Connection): conn: aiormq.Connection = amqp_connection body = os.urandom(int(conn.connection_tune.frame_max * 2.5)) channel: aiormq.Channel = await conn.channel() queue = asyncio.Queue() deaclare_ok = await channel.queue_declare(auto_delete=True) await channel.basic_consume(deaclare_ok.queue, queue.put) await channel.basic_publish(body, routing_key=deaclare_ok.queue) message: DeliveredMessage = await queue.get() assert message.body == body async def test_return_message(amqp_connection: aiormq.Connection): conn: aiormq.Connection = amqp_connection body = os.urandom(512) routing_key = hexlify(os.urandom(16)).decode() channel: aiormq.Channel = await conn.channel( on_return_raises=False, ) result: Optional[Basic.Return] = await channel.basic_publish( body, routing_key=routing_key, mandatory=True, ) assert result is not None assert result.delivery.name == "Basic.Return" assert result.delivery.routing_key == routing_key async def test_cancel_on_queue_deleted(amqp_connection): conn: aiormq.Connection = amqp_connection channel: aiormq.Channel = await conn.channel() deaclare_ok = await channel.queue_declare(auto_delete=True) consume_ok = await channel.basic_consume(deaclare_ok.queue, print) assert consume_ok.consumer_tag in channel.consumers with pytest.raises(aiormq.DuplicateConsumerTag): await channel.basic_consume( deaclare_ok.queue, print, consumer_tag=consume_ok.consumer_tag, ) await channel.queue_delete(deaclare_ok.queue) await asyncio.sleep(0.1) assert consume_ok.consumer_tag not in channel.consumers URL_VHOSTS = [ ("amqp:///", "/"), ("amqp:////", "/"), ("amqp:///test", "test"), ("amqp:////test", "/test"), ("amqp://localhost/test", "test"), ("amqp://localhost//test", "/test"), ("amqps://localhost:5678//test", "/test"), ("amqps://localhost:5678/-test", "-test"), ("amqp://guest:guest@localhost/@test", "@test"), ("amqp://guest:guest@localhost//@test", "/@test"), ] async def test_ssl_verification_fails_without_trusted_ca( amqp_direct_url, rabbitmq_container, ): url = amqp_direct_url.with_scheme("amqps").with_port( rabbitmq_container.ports["5671/tcp"], ) with pytest.raises(ConnectionError, match=".*CERTIFICATE_VERIFY_FAILED.*"): connection = aiormq.Connection(url) await connection.connect() async def test_ssl_context(amqp_direct_url, rabbitmq_container): url = amqp_direct_url.with_scheme("amqps").with_port( rabbitmq_container.ports["5671/tcp"], ) context = ssl.create_default_context( purpose=ssl.Purpose.SERVER_AUTH, cafile=cert_path("ca.pem"), ) context.load_cert_chain(cert_path("client.pem"), cert_path("client.key")) context.check_hostname = False context.verify_mode = ssl.CERT_NONE connection = aiormq.Connection(url, context=context) await connection.connect() await connection.close() @pytest.mark.parametrize("url,vhost", URL_VHOSTS) async def test_connection_urls_vhosts(url, vhost, event_loop): assert aiormq.Connection(url, loop=event_loop).vhost == vhost async def test_update_secret(amqp_connection, amqp_url: URL): respone = await amqp_connection.update_secret( amqp_url.password, timeout=1, ) assert isinstance(respone, aiormq.spec.Connection.UpdateSecretOk) @aiomisc.timeout(20) async def test_connection_stuck(proxy, amqp_url: URL): url = amqp_url.with_host( proxy.proxy_host, ).with_port( proxy.proxy_port, ).update_query(heartbeat="1") async with aiormq.connect(url) as connection: # delay the delivery of each packet by 5 seconds, which # is more than the heartbeat with proxy.slowdown(50, 50): while connection.is_opened: await asyncio.sleep(1) writer_task: asyncio.Task = connection._writer_task # type: ignore assert writer_task.done() with pytest.raises(asyncio.CancelledError): assert writer_task.result() reader_task: asyncio.Task = connection._reader_task # type: ignore assert reader_task.done() with pytest.raises(asyncio.CancelledError): assert reader_task.result() class WriterCapturingTransportFactory(TransportFactory): """Wrap a transport factory and keep the last created StreamWriter.""" def __init__(self, inner: TransportFactory): self.inner = inner self.writer: Optional[asyncio.StreamWriter] = None async def create( self, url: URL, **kwargs: Any, ) -> Tuple[asyncio.StreamReader, asyncio.StreamWriter]: reader, writer = await self.inner.create(url, **kwargs) self.writer = writer return reader, writer @aiomisc.timeout(30) @pytest.mark.parametrize( "scenario", ["server-close", "eof", "eof-reset", "heartbeat-timeout"], ) async def test_writer_closed_on_connection_loss( scenario: str, proxy, amqp_url: URL, event_loop, ): # Regression test for issue #231. The StreamWriter must be closed when # the connection is lost without a client-initiated close. url = amqp_url.with_host( proxy.proxy_host, ).with_port( proxy.proxy_port, ).update_query(heartbeat="1") connection = Connection(url, loop=event_loop) factory = WriterCapturingTransportFactory(connection._transport_factory) connection._transport_factory = factory await connection.connect() writer = factory.writer assert writer is not None assert not writer.is_closing() if scenario == "server-close": # A channel method on channel 0 is a protocol error. The broker # replies with Connection.Close (504 CHANNEL_ERROR). connection.write_queue.put_nowait( ChannelFrame.marshall( channel_number=0, frames=[aiormq.spec.Basic.Ack()], ), ) await asyncio.wait([connection.closing], timeout=10) assert isinstance( connection.closing.exception(), aiormq.exceptions.ConnectionClosed, ) elif scenario == "eof": await proxy.disconnect_all() await asyncio.wait([connection.closing], timeout=10) elif scenario == "eof-reset": # After a TCP reset, write_eof() raises ENOTCONN. The writer must # still be closed. def write_eof() -> None: raise OSError(errno.ENOTCONN, "Transport endpoint is not connected") writer.write_eof = write_eof # type: ignore[method-assign] await proxy.disconnect_all() await asyncio.wait([connection.closing], timeout=10) elif scenario == "heartbeat-timeout": # Delay each packet longer than the heartbeat grace timeout. with proxy.slowdown(50, 50): await asyncio.wait([connection.closing], timeout=20) assert connection.closing.done() assert writer.is_closing(), "StreamWriter is not closed" @aiomisc.timeout(30) @pytest.mark.parametrize("cancel_after", [0.0, 0.1, 0.3, 0.5]) async def test_channel_open_cancelled( cancel_after: float, proxy, amqp_url: URL, event_loop, ): # Regression test for issue #139. A cancelled channel open must not # break the connection and must not leak the socket. url = amqp_url.with_host( proxy.proxy_host, ).with_port( proxy.proxy_port, ) connection = Connection(url, loop=event_loop) factory = WriterCapturingTransportFactory(connection._transport_factory) connection._transport_factory = factory await connection.connect() # Each proxied packet is delayed, so Channel.Open and Confirm.Select # take about 0.4 s each. The cancel points hit both RPC calls. with proxy.slowdown(0.2, 0.2): task = asyncio.ensure_future(connection.channel()) await asyncio.sleep(cancel_after) task.cancel() with pytest.raises(asyncio.CancelledError): await task # The connection must stay usable. channel = await connection.channel() await channel.close() assert not connection.channels await connection.close() assert factory.writer is not None assert factory.writer.is_closing() @aiomisc.timeout(20) async def test_channel_open_cancelled_after_open_ok( amqp_connection: aiormq.Connection, monkeypatch, ): # A cancel between Channel.OpenOk and Confirm.Select must close the # channel on the broker. A local close would leave the broker channel # open, and a later channel with the same number would break the # connection. original_rpc = Channel.rpc async def rpc(self, frame, timeout=None): if isinstance(frame, aiormq.spec.Confirm.Select): task = asyncio.current_task() assert task is not None task.cancel() return await original_rpc(self, frame, timeout=timeout) monkeypatch.setattr(Channel, "rpc", rpc) task = asyncio.ensure_future(amqp_connection.channel()) with pytest.raises(asyncio.CancelledError): await task monkeypatch.undo() numbers = set(amqp_connection.channels) assert len(numbers) == 1, "the number stays reserved until CloseOk" number = numbers.pop() # The broker confirms the close and the number becomes free. while number in amqp_connection.channels: await asyncio.sleep(0.05) channel = await amqp_connection.channel(channel_number=number) await channel.queue_declare(auto_delete=True) await channel.close() FIRST_FRAME_FAILURES = { # The server closes the connection before it sends a frame. "eof": b"", # The server sends bytes that are not an AMQP frame. "garbage": b"HTTP/1.1 400 Bad Request\r\n\r\n", # The server sends a valid frame that is not Connection.Start. "heartbeat": b"\x08\x00\x00\x00\x00\x00\x00\xce", "unknown-type": b"\x09\x00\x00\x00\x00\x00\x01\x00\xce", "protocol-header": b"AMQP\x00\x00\x09\x01", } @aiomisc.timeout(10) @pytest.mark.parametrize("reply", list(FIRST_FRAME_FAILURES)) async def test_connect_first_frame_failure_closes_transport( reply: str, event_loop, ): # Regression test for issue #138. A failure before Connection.Start # is received must close the transport. async def serve( reader: asyncio.StreamReader, writer: asyncio.StreamWriter, ) -> None: await reader.readexactly(8) # the protocol header writer.write(FIRST_FRAME_FAILURES[reply]) await writer.drain() writer.close() server = await asyncio.start_server(serve, "127.0.0.1", 0) port = server.sockets[0].getsockname()[1] try: connection = Connection( URL(f"amqp://guest:guest@127.0.0.1:{port}/"), loop=event_loop, ) factory = WriterCapturingTransportFactory( connection._transport_factory, ) connection._transport_factory = factory with pytest.raises(aiormq.AMQPError): await connection.connect() assert factory.writer is not None assert factory.writer.is_closing(), "transport is not closed" assert connection.is_closed finally: server.close() await server.wait_closed() class BadNetwork: def __init__(self, proxy, stair: int, disconnect_time: float): self.proxy = proxy self.stair = stair self.disconnect_time = disconnect_time self.num_bytes = 0 self.loop = asyncio.get_event_loop() self.lock = asyncio.Lock() proxy.set_content_processors( self.client_to_server, self.server_to_client, ) async def disconnect(self): async with self.lock: await asyncio.sleep(self.disconnect_time) await self.proxy.disconnect_all() self.stair *= 2 self.num_bytes = 0 async def server_to_client(self, chunk: bytes) -> bytes: async with self.lock: self.num_bytes += len(chunk) if self.num_bytes < self.stair: return chunk self.loop.create_task(self.disconnect()) return chunk @staticmethod def client_to_server(chunk: bytes) -> bytes: return chunk DISCONNECT_OFFSETS = [2 << i for i in range(1, 10)] STAIR_STEPS = list( itertools.product([0.0, 0.005, 0.05, 0.1], DISCONNECT_OFFSETS), ) STAIR_STEPS_IDS = [ f"[{i // len(DISCONNECT_OFFSETS)}] {t}-{s}" for i, (t, s) in enumerate(STAIR_STEPS) ] @aiomisc.timeout(30) @pytest.mark.parametrize( "disconnect_time,stair", STAIR_STEPS, ids=STAIR_STEPS_IDS, ) async def test_connection_close_stairway( disconnect_time: float, stair: int, proxy, amqp_url: URL, ): url = amqp_url.with_host( proxy.proxy_host, ).with_port( proxy.proxy_port, ).update_query(heartbeat="1") BadNetwork(proxy, stair, disconnect_time) async def run(): connection = aiormq.Connection(url) await connection.connect() queue = asyncio.Queue() channel = await connection.channel() declare_ok = await channel.queue_declare(auto_delete=True) await channel.basic_consume( declare_ok.queue, queue.put, no_ack=True, ) while True: await channel.basic_publish( b"test", routing_key=declare_ok.queue, ) message: DeliveredMessage = await queue.get() assert message.body == b"test" for _ in range(5): with pytest.raises(aiormq.AMQPError): await run() async def test_ssl_context_provider_static(event_loop): certs = SSLCerts( cert=None, key=None, capath=None, cafile=None, cadata=None, verify=False, ) static_context = ssl.create_default_context() provider = SSLContextProvider( ssl_context=static_context, ssl_certs=certs, loop=event_loop ) provided_context = await provider.get_context() assert provided_context is static_context async def test_ssl_context_provider_created(event_loop): certs = SSLCerts( cert=cert_path("client.pem"), key=cert_path("client.key"), capath=None, cafile=cert_path("ca.pem"), cadata=None, verify=True, ) default_context = ssl.create_default_context() provider = SSLContextProvider( ssl_context=None, ssl_certs=certs, loop=event_loop ) provided_context = await provider.get_context() assert provided_context != default_context second_provided_context = await provider.get_context() assert provided_context is second_provided_context async def test_connection_close_publish(proxy, amqp_url: URL): url = amqp_url.with_host( proxy.proxy_host, ).with_port( proxy.proxy_port, ).update_query(heartbeat="1") async def run(): connection = await aiormq.connect(url) channel = await connection.channel() declare_ok = await channel.queue_declare(auto_delete=True) # This test a bug where a disconnection happening during a call waiting # for the channel lock would result in a deadlock. Here we get the lock # so the call to basic_publish end up holding the lock when we have the # proxy disconnecting. async with channel.lock: task = asyncio.create_task(channel.basic_publish( b"data", routing_key=declare_ok.queue )) await asyncio.sleep(0.5) await proxy.disconnect_all() await asyncio.sleep(0.5) with pytest.raises(aiormq.ChannelInvalidStateError): await task await asyncio.wait_for(run(), timeout=5) async def test_close_waits_for_close_ok(amqp_connection, monkeypatch): connection = amqp_connection close_ok_received = asyncio.Event() handle_close_ok = connection._Connection__handle_close_ok async def on_close_ok(frame): close_ok_received.set() await handle_close_ok(frame) monkeypatch.setattr( connection, "_Connection__handle_close_ok", on_close_ok, ) channel = await connection.channel() await channel.queue_declare(auto_delete=True) await asyncio.wait_for(connection.close(), timeout=5) assert close_ok_received.is_set() # The reader stops by itself after Close-Ok, it is not cancelled. reader_task: asyncio.Task = connection._reader_task # type: ignore assert reader_task.done() assert not reader_task.cancelled() assert reader_task.exception() is None @aiomisc.timeout(20) async def test_close_does_not_wait_forever_for_close_ok( amqp_connection, monkeypatch, ): connection = amqp_connection monkeypatch.setattr(connection, "READER_CLOSE_TIMEOUT", 0.5) async def never_handle_close_ok(_): # Act like Close-Ok never comes back from the broker. await asyncio.Event().wait() monkeypatch.setattr( connection, "_Connection__handle_close_ok", never_handle_close_ok, ) started = connection.loop.time() await asyncio.wait_for(connection.close(), timeout=5) elapsed = connection.loop.time() - started assert connection.is_closed assert 0.5 <= elapsed < 5 reader_task: asyncio.Task = connection._reader_task # type: ignore assert reader_task.done() assert reader_task.cancelled() PARSE_INT_PARAMS = ( (1, 1), ("1", 1), ("0.1", 0), ("-1", -1), ) @pytest.mark.parametrize("value,expected", PARSE_INT_PARAMS) def test_parse_int(value, expected): assert parse_int(value) == expected PARSE_TIMEOUT_PARAMS = ( (1, 1), (1.0, 1), ("0", 0), ("0.0", 0), ("0.111", 0.111), ) @pytest.mark.parametrize("value,expected", PARSE_TIMEOUT_PARAMS) def test_parse_timeout(value, expected): assert parse_timeout(value) == expected PARSE_BOOL_PARAMS = ( ("no", False), ("nope", False), ("do not do it bro", False), ("YES", True), ("yes", True), ("yeS", True), ("yEs", True), ("True", True), ("true", True), ("TRUE", True), ("1", True), ("ENABLE", True), ("ENAbled", True), ("y", True), ("Y", True), ) @pytest.mark.parametrize("value,expected", PARSE_BOOL_PARAMS) def test_parse_bool(value, expected): assert parse_bool(value) == expected async def test_reader_failure_is_logged_with_cause( proxy_connection: aiormq.Connection, proxy, caplog, ): # A frame with an unknown type makes pamqp fail inside the reader task bad_frame = struct.pack(">BHI", 9, 0, 0) + b"\xce" proxy.set_content_processors( lambda chunk: chunk, lambda chunk: bad_frame, ) with caplog.at_level(logging.WARNING, logger="aiormq.connection"): with pytest.raises(Exception): await asyncio.wait_for(proxy_connection.channel(), timeout=5) with pytest.raises(aiormq.AMQPError) as closed: await asyncio.wait_for(proxy_connection.closing, timeout=5) records = [ record for record in caplog.records if record.getMessage().startswith( "Cancelling cause reader exited abnormally", ) ] assert len(records) == 1 assert records[0].levelno == logging.WARNING assert records[0].exc_info is not None if proxy_connection.url.scheme == "amqps": # The garbage breaks the TLS record before pamqp sees it expected_cause = AMQPConnectionError else: expected_cause = aiormq.InvalidFrameError assert isinstance( records[0].exc_info[1].__cause__, pamqp_exceptions.UnmarshalingException, ) assert isinstance(records[0].exc_info[1], expected_cause) assert closed.value is records[0].exc_info[1] @pytest.mark.parametrize("close_error", [False, True]) @pytest.mark.parametrize("target", ["connection", "channel"]) @pytest.mark.parametrize("cancel_method", ["cancel", "timeout"]) @aiomisc.timeout(20) async def test_closing_waiter_cancellation( amqp_url, event_loop, target, cancel_method, close_error, ): amqp_connection = Connection(amqp_url, loop=event_loop) factory = WriterCapturingTransportFactory( amqp_connection._transport_factory, ) amqp_connection._transport_factory = factory await amqp_connection.connect() channel = await amqp_connection.channel() resource = amqp_connection if target == "connection" else channel observer = resource.closing entered = asyncio.Event() async def wait_for_close(): entered.set() await resource.closing waiter = asyncio.create_task(wait_for_close()) await entered.wait() try: if cancel_method == "cancel": waiter.cancel() with pytest.raises(asyncio.CancelledError): await waiter else: with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(waiter, timeout=0.01) assert not resource.is_closed assert not observer.done() # The channel must still support RPC after an observer is cancelled. await channel.basic_qos(prefetch_count=1) reason = AMQPConnectionError("test close") if close_error else None await resource.close(exc=reason) if target == "channel": with pytest.raises(aiormq.ChannelClosed): await observer elif close_error: with pytest.raises(AMQPConnectionError) as caught: await observer assert caught.value is reason else: await observer assert resource.is_closed if target == "connection": assert amqp_connection._reader_task.done() assert amqp_connection._writer_task.done() assert factory.writer.is_closing() else: assert not amqp_connection.is_closed finally: observer.cancel() await amqp_connection.close() @pytest.mark.parametrize("target", ["connection", "channel"]) @pytest.mark.parametrize("cancel_method", ["cancel", "timeout"]) @pytest.mark.parametrize("failure", ["disconnect", "heartbeat-timeout"]) @aiomisc.timeout(30) async def test_closing_waiter_cancelled_before_connection_loss( proxy, amqp_url, event_loop, target, cancel_method, failure, ): url = amqp_url.with_host(proxy.proxy_host).with_port( proxy.proxy_port, ).update_query(heartbeat="1") connection = Connection(url, loop=event_loop) factory = WriterCapturingTransportFactory(connection._transport_factory) connection._transport_factory = factory await connection.connect() channel = await connection.channel() resource = connection if target == "connection" else channel # Include heartbeat and channel tasks, not just the transport reader/writer. background = tuple(connection._Base__future_store.futures) observer = resource.closing entered = asyncio.Event() async def wait_for_close(): entered.set() await resource.closing waiter = asyncio.create_task(wait_for_close()) await entered.wait() try: if cancel_method == "cancel": waiter.cancel() with pytest.raises(asyncio.CancelledError): await waiter else: with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(waiter, timeout=0.01) assert not resource.is_closed assert not observer.done() await channel.basic_qos(prefetch_count=1) if failure == "disconnect": await proxy.disconnect_all() with pytest.raises(AMQPConnectionError): await asyncio.wait_for(observer, timeout=10) else: # Keep TCP open but delay traffic beyond the heartbeat grace time. with proxy.slowdown(50, 50): with pytest.raises(asyncio.CancelledError): await asyncio.wait_for(observer, timeout=20) # No explicit close() before these checks: the network failure must # complete cleanup even after the first observer was cancelled. await asyncio.wait_for( asyncio.gather(*background, return_exceptions=True), timeout=5, ) assert connection.is_closed assert channel.is_closed assert connection._reader_task.done() assert connection._writer_task.done() assert factory.writer.is_closing() finally: observer.cancel() await connection.close() mosquito-aiormq-612e0d0/tests/test_connection_futures.py000066400000000000000000000031201527161700100236660ustar00rootroot00000000000000import asyncio import gc import weakref import pytest from aiormq.exceptions import ConnectionClosed async def test_connection_close_during_publish_drain( amqp_connection, monkeypatch, event_loop, ): channel = await amqp_connection.channel() entered = asyncio.Event() contexts = [] previous = event_loop.get_exception_handler() event_loop.set_exception_handler( lambda loop, context: contexts.append(context), ) class Queue: async def put(self, frame): # Hold the frame before the writer drains it. Connection shutdown # rejects both futures while publish is only awaiting the drain. entered.set() try: with monkeypatch.context() as patch: patch.setattr(channel, "write_queue", Queue()) task = asyncio.create_task(channel.basic_publish(b"test")) await asyncio.wait_for(entered.wait(), 2) confirmation = weakref.ref(channel.confirmations[1]) error = ConnectionClosed(320, "CONNECTION_FORCED") await amqp_connection.close(error) with pytest.raises(ConnectionClosed): await task # Closing futures retain the shared exception. Drop its traceback # so the publish frame does not keep the confirmation alive. error.__traceback__ = None del task, error channel.confirmations.clear() await asyncio.sleep(0) gc.collect() assert not contexts assert confirmation() is None finally: event_loop.set_exception_handler(previous) mosquito-aiormq-612e0d0/tests/test_consumer_cancel.py000066400000000000000000000040611527161700100231170ustar00rootroot00000000000000import asyncio import aiomisc import pytest from aiormq.exceptions import ChannelInvalidStateError @pytest.mark.parametrize("method", ["basic_ack", "basic_nack", "basic_reject"]) @pytest.mark.parametrize("wait", [False, True]) @pytest.mark.parametrize("cause", ["task-cancel", "channel-close"]) @aiomisc.timeout(20) async def test_ack_from_cancelled_consumer( proxy_connection, method, wait, cause, ): connection = proxy_connection setup = await connection.channel() consumer = await connection.channel() queue = await setup.queue_declare(exclusive=True) entered = asyncio.get_running_loop().create_future() result = asyncio.get_running_loop().create_future() async def callback(message): entered.set_result(asyncio.current_task()) try: await asyncio.Event().wait() except asyncio.CancelledError: try: await getattr(message.channel, method)( message.delivery_tag, wait=wait, ) except BaseException as exc: result.set_result(exc) else: result.set_result(None) tag = await consumer.basic_consume(queue.queue, callback) await setup.basic_publish(b"pending", routing_key=queue.queue) callback_task = await entered await consumer.basic_cancel(tag.consumer_tag) if cause == "channel-close": await consumer.close() else: callback_task.cancel() error = await result # An ack after Channel.CloseOk must not make RabbitMQ close the connection. await setup.basic_qos(prefetch_count=1) assert not connection.is_closed if cause == "channel-close": assert isinstance(error, ChannelInvalidStateError) else: assert error is None await consumer.basic_qos(prefetch_count=1) message = await setup.basic_get(queue.queue, no_ack=True) if cause == "task-cancel" and method == "basic_ack": assert message.delivery_tag is None else: assert message.body == b"pending" assert message.delivery.redelivered mosquito-aiormq-612e0d0/tests/test_consumer_exceptions.py000066400000000000000000000061231527161700100240540ustar00rootroot00000000000000import asyncio import gc import logging import aiomisc import pytest from aiormq.exceptions import AMQPConnectionError @pytest.mark.parametrize( "outcome", ["error", "cancel", "success", "disconnect"], ) @aiomisc.timeout(20) async def test_consumer_exception_is_reported_once( proxy_connection, proxy, monkeypatch, caplog, outcome, ): connection = proxy_connection setup = await connection.channel() channel = await connection.channel() queue = await setup.queue_declare(exclusive=True) finished = asyncio.Event() entered = asyncio.Event() retained = [] error = ValueError("consumer failed") create_task = channel.create_task def capture_task(coro): task = create_task(coro) if coro.__name__ == "callback": # Keep the task alive: error reporting must not depend on GC. retained.append(task) task.add_done_callback(lambda _: finished.set()) return task async def callback(message): if outcome == "disconnect": entered.set() try: await asyncio.Event().wait() except asyncio.CancelledError: raise error if outcome == "error": raise error if outcome == "cancel": raise asyncio.CancelledError await channel.basic_ack(message.delivery_tag) monkeypatch.setattr(channel, "create_task", capture_task) with caplog.at_level(logging.ERROR, logger="aiormq.channel"): consume = await channel.basic_consume(queue.queue, callback) await setup.basic_publish(b"input", routing_key=queue.queue) if outcome == "disconnect": await entered.wait() await proxy.disconnect_all() with pytest.raises(AMQPConnectionError): await connection.closing await finished.wait() if outcome != "disconnect": await setup.basic_qos(prefetch_count=1) assert not connection.is_closed assert not channel.is_closed records = [ record for record in caplog.records if record.getMessage() == "Consumer callback failed" ] assert len(records) == (1 if outcome in ("error", "disconnect") else 0) if records: assert records[0].exc_info[1] is error # Reporting must not consume the exception for an explicit waiter. with pytest.raises(ValueError) as caught: await retained[0] assert caught.value is error retained.clear() gc.collect() if outcome != "disconnect": await channel.basic_cancel(consume.consumer_tag) await channel.close() message = await setup.basic_get(queue.queue, no_ack=True) if outcome == "success": assert message.delivery_tag is None else: assert message.body == b"input" assert message.delivery.redelivered assert sum( record.getMessage() == "Consumer callback failed" for record in caplog.records ) == len(records) mosquito-aiormq-612e0d0/tests/test_consumer_publish.py000066400000000000000000000114121527161700100233360ustar00rootroot00000000000000import asyncio import uuid import aiomisc import pytest from aiormq.exceptions import ChannelAccessRefused @pytest.mark.parametrize("separate_publisher", [False, True]) @pytest.mark.parametrize("wait", [False, True]) @pytest.mark.parametrize("timeout", [None, 2]) @aiomisc.timeout(15) async def test_consumer_can_catch_publish_error( amqp_connection, separate_publisher, wait, timeout, ): setup = await amqp_connection.channel() queue = await setup.queue_declare(exclusive=True) exchange = "internal-" + uuid.uuid4().hex await setup.exchange_declare(exchange, internal=True) consumer = await amqp_connection.channel() publisher = ( await amqp_connection.channel() if separate_publisher else consumer ) result = asyncio.get_running_loop().create_future() async def callback(message): try: await publisher.basic_publish( b"response", exchange=exchange, wait=wait, timeout=timeout, ) except Exception as exc: # This is the user's exception handler, inside the consumer # task, rather than an observer awaiting a TaskWrapper. if separate_publisher: await consumer.basic_ack(message.delivery_tag) result.set_result((exc, asyncio.current_task().cancelling())) except BaseException as exc: result.set_result((exc, asyncio.current_task().cancelling())) else: result.set_result((None, asyncio.current_task().cancelling())) try: await consumer.basic_consume(queue.queue, callback) await setup.basic_publish(b"request", routing_key=queue.queue) exc, cancelling = await result assert isinstance(exc, ChannelAccessRefused) assert cancelling == 0 if not separate_publisher: assert consumer.is_closed # The failed callback must not silently acknowledge the input. message = await setup.basic_get(queue.queue, no_ack=True) assert message.body == b"request" assert message.delivery.redelivered else: assert not consumer.is_closed finally: await setup.exchange_delete(exchange) @pytest.mark.parametrize("close_connection", [False, True]) @aiomisc.timeout(15) async def test_publish_shutdown_remains_cancelled( amqp_connection, monkeypatch, close_connection, ): channel = await amqp_connection.channel() entered = asyncio.Event() result = asyncio.get_running_loop().create_future() queue = await channel.queue_declare(exclusive=True) def delay_confirm(delivery_tag, frame): # Let the writer finish and release the publish lock, then keep # the callback waiting for confirmation when shutdown starts. entered.set() async def callback(): try: await channel.basic_publish(b"sent", routing_key=queue.queue) except BaseException as exc: result.set_result(exc) with monkeypatch.context() as patch: patch.setattr(channel, "_confirm_delivery", delay_confirm) channel.create_task(callback()) await entered.wait() if close_connection: await amqp_connection.close() else: await channel.close() assert isinstance(await result, asyncio.CancelledError) @pytest.mark.parametrize("external", ["before", "after", "only"]) @aiomisc.timeout(15) async def test_publish_preserves_external_cancellation( amqp_connection, monkeypatch, external, ): channel = await amqp_connection.channel() entered = asyncio.Event() result = asyncio.get_running_loop().create_future() reason = ChannelAccessRefused("broker refusal") class BlockingQueue: async def put(self, item): entered.set() await asyncio.Event().wait() async def callback(): try: await channel.basic_publish(b"not sent") except BaseException as exc: result.set_result(exc) with monkeypatch.context() as patch: patch.setattr(channel, "write_queue", BlockingQueue()) # Keep this task outside FutureStore to control the two competing # cancellation requests without also closing the real transport. task = asyncio.create_task(callback()) await entered.wait() if external != "only": channel._close_exception = reason channel._closing.set_exception(reason) if external == "before": task.cancel("user cancellation") task.cancel(reason) elif external == "after": task.cancel(reason) task.cancel("user cancellation") else: # An exception passed by the user is not a channel close. task.cancel(reason) await task assert isinstance(await result, asyncio.CancelledError) mosquito-aiormq-612e0d0/tests/test_delivery_error.py000066400000000000000000000045441527161700100230210ustar00rootroot00000000000000import pytest from pamqp.commands import Basic from pamqp.header import ContentHeader from aiormq.abc import DeliveredMessage from aiormq.exceptions import DeliveryError, PublishError @pytest.mark.parametrize("frame, details", [ (Basic.Nack(delivery_tag=42, multiple=True), ("Basic.Nack", "delivery_tag=42", "multiple=True")), (Basic.Reject(delivery_tag=7, requeue=False), ("Basic.Reject", "delivery_tag=7", "requeue=False")), (Basic.Return(reply_code=312, reply_text="NO_ROUTE", routing_key="missing"), ("Basic.Return", "reply_code=312", "NO_ROUTE", "missing")), ]) def test_delivery_error_details_preserve_attributes(frame, details): error = DeliveryError(None, frame) assert error.args == (None, frame) assert error.message is None assert error.frame is frame for detail in details: assert detail in str(error) assert detail in repr(error) assert "object at" not in str(error) def test_publish_error_details_preserve_attributes(): frame = Basic.Return( reply_code=312, reply_text="NO_ROUTE", exchange="events", routing_key="missing", ) message = DeliveredMessage( delivery=frame, header=ContentHeader(), body=b"private payload", channel=None, ) error = PublishError(message, frame) assert error.args == ("NO_ROUTE", "missing") assert error.message is message assert error.frame is frame for detail in ("Basic.Return", "reply_code=312", "NO_ROUTE", "events"): assert detail in str(error) assert "PublishError" in repr(error) assert "private payload" not in repr(error) async def test_broker_nack_details(amqp_connection): channel = await amqp_connection.channel() queue = await channel.queue_declare( exclusive=True, arguments={"x-max-length": 1, "x-overflow": "reject-publish"}, ) await channel.basic_publish(b"accepted", routing_key=queue.queue) with pytest.raises(DeliveryError) as caught: await channel.basic_publish(b"rejected", routing_key=queue.queue) error = caught.value assert isinstance(error.frame, Basic.Nack) assert error.frame.delivery_tag == 2 assert "Basic.Nack" in str(error) assert "delivery_tag=2" in str(error) assert error.args == (None, error.frame) message = await channel.basic_get(queue.queue, no_ack=True) assert message.body == b"accepted" mosquito-aiormq-612e0d0/tests/test_future_store.py000066400000000000000000000107761527161700100225170ustar00rootroot00000000000000import asyncio import pytest from aiormq.abc import TaskWrapper from aiormq.base import FutureStore @pytest.fixture def root_store(event_loop): store = FutureStore(loop=event_loop) try: yield store finally: event_loop.run_until_complete( store.reject_all(Exception("Cancelling")), ) @pytest.fixture def child_store(event_loop, root_store): store = root_store.get_child() try: yield store finally: event_loop.run_until_complete( store.reject_all(Exception("Cancelling")), ) async def test_reject_all(root_store: FutureStore, child_store: FutureStore): future1 = root_store.create_future() future2 = child_store.create_future() assert root_store.futures assert child_store.futures await root_store.reject_all(RuntimeError) await asyncio.sleep(0.1) assert isinstance(future1.exception(), RuntimeError) assert isinstance(future2.exception(), RuntimeError) assert not root_store.futures assert not child_store.futures async def test_result(root_store: FutureStore, child_store: FutureStore): async def result(): await asyncio.sleep(0.1) return "result" assert await child_store.create_task(result()) == "result" async def test_siblings(root_store: FutureStore, child_store: FutureStore): async def coro(store): await asyncio.sleep(0.1) await store.reject_all(RuntimeError) task1 = child_store.create_task(coro(child_store)) assert root_store.futures assert child_store.futures with pytest.raises(RuntimeError): await task1 await asyncio.sleep(0.1) assert not root_store.futures assert not child_store.futures child = child_store.get_child().get_child().get_child() task = child.create_task(coro(child)) assert root_store.futures assert child_store.futures assert child.futures with pytest.raises(RuntimeError): await task await asyncio.sleep(0.1) assert not root_store.futures assert not child_store.futures assert not child.futures async def test_task_wrapper(event_loop): future = event_loop.create_future() wrapped = TaskWrapper(future) wrapped.throw(RuntimeError()) with pytest.raises(asyncio.CancelledError): await future with pytest.raises(RuntimeError): await wrapped @pytest.mark.parametrize( "reason", [RuntimeError("boom"), RuntimeError, asyncio.CancelledError()], ids=["instance", "class", "cancelled"], ) async def test_task_wrapper_throw_reason(event_loop, reason): # The cancelled task sees the reason in its CancelledError. The # wrapper raises the reason itself. async def work() -> None: await asyncio.sleep(1) task = event_loop.create_task(work()) wrapped = TaskWrapper(task) wrapped.throw(reason) with pytest.raises(asyncio.CancelledError) as exc_info: await task assert exc_info.value.args == (reason,) expected = reason if isinstance(reason, type) else type(reason) with pytest.raises(expected) as exc_info: await wrapped if not isinstance(reason, type): assert exc_info.value is reason @pytest.mark.parametrize("child", [False, True]) @pytest.mark.parametrize("outcome", ["reject", "exception", "cancel", "result"]) async def test_unobserved_future(event_loop, root_store, child, outcome): import gc import weakref store = root_store.get_child() if child else root_store contexts = [] previous = event_loop.get_exception_handler() event_loop.set_exception_handler( lambda loop, context: contexts.append(context), ) try: future = store.create_future() reference = weakref.ref(future) if outcome == "reject": await root_store.reject_all(RuntimeError("closed")) elif outcome == "exception": future.set_exception(RuntimeError("failed")) elif outcome == "cancel": future.cancel() else: future.set_result(None) await asyncio.sleep(0) del future gc.collect() assert not contexts assert reference() is None finally: event_loop.set_exception_handler(previous) async def test_retrieved_exception_still_reaches_waiter(event_loop, root_store): future = root_store.create_future() error = RuntimeError("closed") await root_store.reject_all(error) await asyncio.sleep(0) with pytest.raises(RuntimeError) as caught: await future assert caught.value is error mosquito-aiormq-612e0d0/tests/test_late_confirms.py000066400000000000000000000053151527161700100226070ustar00rootroot00000000000000import asyncio import logging import aiomisc import pytest from pamqp.commands import Basic @pytest.mark.parametrize("failure", ["cancel", "timeout"]) @aiomisc.timeout(15) async def test_late_confirm_after_cancel( amqp_connection, monkeypatch, caplog, failure, ): channel = await amqp_connection.channel() queue = await channel.queue_declare(exclusive=True) received = asyncio.get_running_loop().create_future() on_confirm = channel._confirm_delivery def hold_confirm(delivery_tag, frame): received.set_result(frame) with monkeypatch.context() as patch: patch.setattr(channel, "_confirm_delivery", hold_confirm) publish = asyncio.create_task(channel.basic_publish( b"sent", routing_key=queue.queue, wait=False, timeout=0.2 if failure == "timeout" else None, )) frame = await received confirmation = channel.confirmations[1] if failure == "cancel": publish.cancel() with pytest.raises( TimeoutError if failure == "timeout" else asyncio.CancelledError, ): await publish assert confirmation.cancelled() with caplog.at_level(logging.DEBUG, logger="aiormq.channel"): # Release the actual broker confirmation after its waiter has gone. on_confirm(frame.delivery_tag, frame) await asyncio.sleep(0) assert not channel.confirmations ignored = [r for r in caplog.records if "was ignored" in r.getMessage()] assert len(ignored) == 1 assert ignored[0].levelno == logging.DEBUG result = await channel.basic_publish(b"next", routing_key=queue.queue) assert isinstance(result, Basic.Ack) assert result.delivery_tag == 2 for body in (b"sent", b"next"): message = await channel.basic_get(queue.queue, no_ack=True) assert message.body == body @pytest.mark.parametrize("cancelled", [False, True]) async def test_multiple_confirm_with_completed_waiter( amqp_connection, caplog, cancelled, ): channel = await amqp_connection.channel() completed = channel.create_future() pending = channel.create_future() channel.confirmations.update({1: completed, 2: pending}) if cancelled: completed.cancel() else: completed.set_result(Basic.Ack(delivery_tag=1)) frame = Basic.Ack(delivery_tag=2, multiple=True) with caplog.at_level(logging.DEBUG, logger="aiormq.channel"): await channel._on_confirm_frame(frame) assert await pending is frame assert not channel.confirmations ignored = [r for r in caplog.records if "was ignored" in r.getMessage()] assert len(ignored) == 1 assert ignored[0].levelno == ( logging.DEBUG if cancelled else logging.WARNING ) mosquito-aiormq-612e0d0/tests/test_nowait.py000066400000000000000000000136161527161700100212660ustar00rootroot00000000000000import uuid from typing import Any, Callable, Coroutine, Dict, NamedTuple, Optional import aiomisc import pytest from pamqp import commands as spec from aiormq.abc import AbstractChannel, AbstractConnection def unique(prefix: str) -> str: return f"{prefix}-{uuid.uuid4().hex}" class Targets(NamedTuple): queue: str source: str destination: str async def make_targets(channel: AbstractChannel) -> Targets: """Declare the queue and the exchanges which the no-wait calls change.""" queue = unique("nowait-queue") source = unique("nowait-source") destination = unique("nowait-destination") await channel.queue_declare(queue, exclusive=True) for exchange in (source, destination): await channel.exchange_declare( exchange, exchange_type="fanout", auto_delete=True, ) await channel.exchange_bind(destination, source) await channel.queue_bind(queue, source) return Targets(queue=queue, source=source, destination=destination) def make_call( channel: AbstractChannel, method: str, targets: Targets, nowait: bool, ) -> Callable[[], Coroutine[Any, Any, Any]]: calls: Dict[str, Callable[[], Coroutine[Any, Any, Any]]] = { "queue_declare": lambda: channel.queue_declare( unique("nowait-declare"), exclusive=True, nowait=nowait, ), "queue_bind": lambda: channel.queue_bind( targets.queue, targets.source, routing_key="key", nowait=nowait, ), "queue_purge": lambda: channel.queue_purge( targets.queue, nowait=nowait, ), "queue_delete": lambda: channel.queue_delete( targets.queue, nowait=nowait, ), "exchange_declare": lambda: channel.exchange_declare( unique("nowait-exchange"), auto_delete=True, nowait=nowait, ), "exchange_bind": lambda: channel.exchange_bind( targets.destination, targets.source, routing_key="key", nowait=nowait, ), "exchange_unbind": lambda: channel.exchange_unbind( targets.destination, targets.source, nowait=nowait, ), "exchange_delete": lambda: channel.exchange_delete( targets.destination, nowait=nowait, ), } return calls[method] EXPECTED_RESPONSES: Dict[str, Any] = { "queue_declare": spec.Queue.DeclareOk, "queue_bind": spec.Queue.BindOk, "queue_purge": spec.Queue.PurgeOk, "queue_delete": spec.Queue.DeleteOk, "exchange_declare": spec.Exchange.DeclareOk, "exchange_bind": spec.Exchange.BindOk, "exchange_unbind": spec.Exchange.UnbindOk, "exchange_delete": spec.Exchange.DeleteOk, } @pytest.mark.parametrize("method", sorted(EXPECTED_RESPONSES)) @pytest.mark.parametrize("nowait", [False, True]) @aiomisc.timeout(20) async def test_nowait_returns_without_reply( amqp_connection: AbstractConnection, method: str, nowait: bool, ) -> None: channel = await amqp_connection.channel() targets = await make_targets(channel) result = await make_call(channel, method, targets, nowait)() if nowait: # The broker sends no reply for a no-wait call. assert result is None else: assert isinstance(result, EXPECTED_RESPONSES[method]) # The call must release the RPC lock and keep the channel usable. await channel.basic_qos(prefetch_count=1) assert not channel.is_closed assert not amqp_connection.is_closed @pytest.mark.parametrize("nowait", [False, True]) @aiomisc.timeout(20) async def test_basic_cancel_nowait( amqp_connection: AbstractConnection, nowait: bool, ) -> None: channel = await amqp_connection.channel() queue = unique("nowait-consume") await channel.queue_declare(queue, exclusive=True) async def callback(message: Any) -> None: # pragma: no cover await message.channel.basic_ack(message.delivery_tag) first = await channel.basic_consume(queue, callback) result = await channel.basic_cancel(first.consumer_tag, nowait=nowait) if nowait: assert result is None else: assert isinstance(result, spec.Basic.CancelOk) # The channel forgets a cancelled consumer, with or without a reply. assert first.consumer_tag not in channel.consumers # A second cancel on the same channel must also complete. second = await channel.basic_consume(queue, callback) await channel.basic_cancel(second.consumer_tag, nowait=nowait) assert second.consumer_tag not in channel.consumers declare_ok = await channel.queue_declare(queue, passive=True) assert declare_ok.consumer_count == 0 assert not channel.is_closed assert not amqp_connection.is_closed @pytest.mark.parametrize("nowait", [False, True]) @aiomisc.timeout(20) async def test_basic_recover_nowait( amqp_connection: AbstractConnection, nowait: bool, ) -> None: channel = await amqp_connection.channel() if nowait: # A no-wait recover sends Basic.RecoverAsync. AMQP 0-9-1 deprecates # that command, and the broker sends no reply for it. with pytest.warns(DeprecationWarning): result = await channel.basic_recover(requeue=True, nowait=True) assert result is None else: assert isinstance( await channel.basic_recover(requeue=True), spec.Basic.RecoverOk, ) await channel.basic_qos(prefetch_count=1) assert not channel.is_closed assert not amqp_connection.is_closed @pytest.mark.parametrize("nowait", [False, True]) @aiomisc.timeout(20) async def test_confirm_delivery_nowait( amqp_connection: AbstractConnection, nowait: bool, ) -> None: channel = await amqp_connection.channel(publisher_confirms=False) result: Optional[Any] = await channel.confirm_delivery(nowait=nowait) if nowait: assert result is None else: assert isinstance(result, spec.Confirm.SelectOk) await channel.basic_qos(prefetch_count=1) assert not channel.is_closed assert not amqp_connection.is_closed mosquito-aiormq-612e0d0/tests/test_protocol_errors.py000066400000000000000000000042361527161700100232200ustar00rootroot00000000000000import asyncio from unittest.mock import AsyncMock, Mock import pytest from pamqp.commands import Connection as ConnectionFrames from pamqp.exceptions import UnmarshalingException from pamqp.heartbeat import Heartbeat from aiormq import AMQPError, InvalidFrameError, ProtocolSyntaxError from aiormq.abc import TaskWrapper from aiormq.connection import Connection, FrameReceiver @pytest.mark.parametrize("payload", [ b"\x09\x00\x00\x00\x00\x00\x01\x00\xce", # Unknown frame type. b"\x03\x00\x01\x00\x00\x00\x01\x00\x00", # Invalid terminator. ]) async def test_decode_error_belongs_to_aiormq_hierarchy(payload): reader = asyncio.StreamReader() reader.feed_data(payload) reader.feed_eof() with pytest.raises(AMQPError) as caught: await FrameReceiver(reader).get_frame() assert isinstance(caught.value, InvalidFrameError) assert isinstance(caught.value.__cause__, UnmarshalingException) assert str(caught.value.__cause__) in str(caught.value) async def test_protocol_header_belongs_to_aiormq_hierarchy(): reader = asyncio.StreamReader() reader.feed_data(b"AMQP\x00\x00\x09\x01") reader.feed_eof() with pytest.raises(AMQPError) as caught: await FrameReceiver(reader).get_frame() assert isinstance(caught.value, ProtocolSyntaxError) async def test_unexpected_handshake_response_belongs_to_aiormq_hierarchy(): writer = Mock(drain=AsyncMock()) receiver = Mock(get_frame=AsyncMock(return_value=(8, 0, Heartbeat()))) with pytest.raises(AMQPError) as caught: await Connection._rpc(ConnectionFrames.Open(), writer, receiver) assert isinstance(caught.value, InvalidFrameError) assert "Connection.OpenOk" in str(caught.value) async def test_shutdown_preserves_protocol_error_cause(): cause = UnmarshalingException("Unknown", "Invalid frame") try: raise InvalidFrameError(str(cause)) from cause except InvalidFrameError as error: reason = error task = TaskWrapper(asyncio.create_task(asyncio.sleep(10))) task.throw(reason) with pytest.raises(AMQPError) as caught: await task assert caught.value is reason assert caught.value.__cause__ is cause mosquito-aiormq-612e0d0/tests/test_publish_cancel.py000066400000000000000000000112141527161700100227300ustar00rootroot00000000000000import asyncio import uuid from dataclasses import replace import aiomisc import pamqp.frame import pytest import aiormq @pytest.mark.parametrize("failure", ["cancel", "timeout", "marshal"]) @pytest.mark.parametrize("confirms", [False, True]) @pytest.mark.parametrize("wait", [False, True]) @aiomisc.timeout(15) async def test_publish_failure_before_send( amqp_connection, monkeypatch, failure, confirms, wait, ): channel = await amqp_connection.channel(publisher_confirms=confirms) queue = await channel.queue_declare(exclusive=True) original_queue = channel.write_queue entered = asyncio.Event() futures = [] create_future = channel.create_future def record_future(): future = create_future() futures.append(future) return future class BlockingQueue: async def put(self, item): entered.set() await asyncio.Event().wait() marshal = pamqp.frame.marshal def broken_marshal(frame, *args, **kwargs): if isinstance(frame, aiormq.spec.Basic.Publish): raise ValueError("Cannot encode publish") return marshal(frame, *args, **kwargs) with monkeypatch.context() as patch: patch.setattr(channel, "create_future", record_future) if failure == "marshal": patch.setattr(pamqp.frame, "marshal", broken_marshal) else: patch.setattr(channel, "write_queue", BlockingQueue()) publish = asyncio.create_task(channel.basic_publish( b"not sent", routing_key=queue.queue, properties=aiormq.spec.Basic.Properties(message_id="retry"), wait=wait, timeout=0.1 if failure == "timeout" else None, )) if failure != "marshal": await entered.wait() if failure == "cancel": publish.cancel() error = { "cancel": asyncio.CancelledError, "timeout": TimeoutError, "marshal": ValueError, }[failure] with pytest.raises(error): await publish assert channel.write_queue is original_queue assert channel.delivery_tag == 0 assert not channel.confirmations assert not channel.message_id_delivery_tag assert all(future.cancelled() for future in futures) # Reusing the message ID must not let an old cleanup callback remove # the new publication's mapping. Return handling needs this mapping. if confirms: with pytest.raises(aiormq.exceptions.PublishError): await channel.basic_publish( b"returned", routing_key=uuid.uuid4().hex, mandatory=True, properties=aiormq.spec.Basic.Properties(message_id="retry"), timeout=2, ) result = await channel.basic_publish( b"next", routing_key=queue.queue, timeout=2, ) if confirms: assert isinstance(result, aiormq.spec.Basic.Ack) assert result.delivery_tag == channel.delivery_tag message = await channel.basic_get(queue.queue, no_ack=True, timeout=2) assert message.body == b"next" await channel.close() @pytest.mark.parametrize( "failure", ["cancel", "timeout", "cancel_at_enqueue"], ) @aiomisc.timeout(15) async def test_publish_failure_after_enqueue( amqp_connection, monkeypatch, failure, ): channel = await amqp_connection.channel() queue = await channel.queue_declare(exclusive=True) original_queue = channel.write_queue enqueued = asyncio.Event() class DelayedDrainQueue: async def put(self, item): # Deliver the frame to the real broker, but leave the caller # waiting for drain, after its sequence number has been used. await original_queue.put(replace(item, drain_future=None)) enqueued.set() if failure == "cancel_at_enqueue": publish.cancel() with monkeypatch.context() as patch: patch.setattr(channel, "write_queue", DelayedDrainQueue()) publish = asyncio.create_task(channel.basic_publish( b"sent", routing_key=queue.queue, timeout=0.1 if failure == "timeout" else 5, )) await enqueued.wait() if failure == "cancel": publish.cancel() with pytest.raises( TimeoutError if failure == "timeout" else asyncio.CancelledError, ): await publish assert channel.delivery_tag == 1 result = await channel.basic_publish( b"next", routing_key=queue.queue, timeout=2, ) assert result.delivery_tag == 2 for body in (b"sent", b"next"): message = await channel.basic_get(queue.queue, no_ack=True, timeout=2) assert message.body == body await channel.close() mosquito-aiormq-612e0d0/tests/test_tools.py000066400000000000000000000043041527161700100211170ustar00rootroot00000000000000import asyncio import pytest from aiormq.tools import Countdown, awaitable def simple_func(): return 1 def await_result_func(): return asyncio.sleep(0) async def await_func(): await asyncio.sleep(0) return 2 def return_future(): loop = asyncio.get_event_loop() f = loop.create_future() loop.call_soon(f.set_result, 5) return f async def await_future(): return (await return_future()) + 1 def return_coroutine(): return await_future() AWAITABLE_FUNCS = [ (simple_func, 1), (await_result_func, None), (await_func, 2), (return_future, 5), (await_future, 6), (return_coroutine, 6), ] @pytest.mark.parametrize("func,result", AWAITABLE_FUNCS) async def test_awaitable(func, result): assert await awaitable(func)() == result async def test_countdown(): countdown = Countdown(timeout=0.1) await countdown(asyncio.sleep(0)) # waiting for the countdown exceeded await asyncio.sleep(0.2) task = asyncio.create_task(asyncio.sleep(0)) with pytest.raises(asyncio.TimeoutError): await countdown(task) assert task.cancelled() async def test_countdown_context_releases_lock_after_timeout_inside(): countdown = Countdown(timeout=0.05) lock = asyncio.Lock() with pytest.raises(asyncio.TimeoutError): async with countdown.enter_context(lock): assert lock.locked() # The deadline expires here, like a slow write_queue.put() await countdown(asyncio.sleep(1)) assert not lock.locked() async def test_countdown_context_releases_lock_after_deadline(): countdown = Countdown(timeout=0.05) lock = asyncio.Lock() async with countdown.enter_context(lock): assert lock.locked() # The body completes, but the deadline is already in the past await asyncio.sleep(0.1) assert not lock.locked() async def test_countdown_context_does_not_enter_after_deadline(): countdown = Countdown(timeout=0.05) lock = asyncio.Lock() await asyncio.sleep(0.1) with pytest.raises(asyncio.TimeoutError): async with countdown.enter_context(lock): pytest.fail("The context body must not run") assert not lock.locked() mosquito-aiormq-612e0d0/tests/test_writer_shutdown.py000066400000000000000000000043531527161700100232320ustar00rootroot00000000000000import asyncio import pytest @pytest.mark.parametrize("concurrent_close", [False, True]) async def test_close_waits_for_writer_cleanup( amqp_connection, monkeypatch, concurrent_close, ): connection = amqp_connection cleanup_started = asyncio.Event() release_cleanup = asyncio.Event() reader_handled = asyncio.Event() cleanup_finished = False cleanup_cancelled = False close_writer = connection._Connection__close_writer reader_done = connection._on_reader_done async def delayed_close(writer): nonlocal cleanup_finished, cleanup_cancelled cleanup_started.set() try: await release_cleanup.wait() await close_writer(writer) cleanup_finished = True except asyncio.CancelledError: cleanup_cancelled = True raise def on_reader_done(task): reader_done(task) # Run after the reader callback's close_writer_task has started. connection.loop.call_soon(reader_handled.set) # connect() already registered the original bound callback. connection._reader_task.remove_done_callback(reader_done) connection._reader_task.add_done_callback(on_reader_done) monkeypatch.setattr(connection, "_Connection__close_writer", delayed_close) closers = [asyncio.create_task(connection.close())] try: await asyncio.wait_for(cleanup_started.wait(), 5) await asyncio.wait_for(reader_handled.wait(), 5) if concurrent_close: second_started = asyncio.Event() on_close = connection._on_close async def second_on_close(exc): connection.loop.call_soon(second_started.set) await on_close(exc) monkeypatch.setattr(connection, "_on_close", second_on_close) closers.append(asyncio.create_task(connection.close())) await asyncio.wait_for(second_started.wait(), 5) assert not cleanup_cancelled assert not cleanup_finished assert all(not closer.done() for closer in closers) finally: release_cleanup.set() await asyncio.wait_for(asyncio.gather(*closers), 5) assert cleanup_finished assert not cleanup_cancelled assert connection._writer_task.done() mosquito-aiormq-612e0d0/uv.lock000066400000000000000000010220251527161700100165110ustar00rootroot00000000000000version = 1 revision = 5 requires-python = ">=3.11, <4" resolution-markers = [ "python_full_version >= '3.15'", "python_full_version < '3.15'", ] [[package]] name = "aiomisc" version = "18.0.28" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiothreads" }, { name = "colorlog" }, { name = "logging-journald", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/17/b63b8354b6a3c88d5a67db70e5b4ed0a5a37bd24580c5d1ac7faf5b4dbfc/aiomisc-18.0.28.tar.gz", hash = "sha256:480fe2243db713acded53bb3c9c0d04dc5b9fe72e83204dd17dab386b0556934", size = 507387, upload-time = "2026-09-06T13:27:38.137Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/43/a455195c7ba20f08effa5d5c53807dd4721734146524df5f26d1dbad1b97/aiomisc-18.0.28-py3-none-any.whl", hash = "sha256:0db40558fca3c1ee2eaac2235b8a71f3b222144932903bd4197b6e901cb040d4", size = 94930, upload-time = "2026-09-06T13:27:36.567Z" }, ] [[package]] name = "aiomisc-pytest" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiomisc" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/41/083ee3de2aafdf20895bac41a535f3425852098a5130ed1681ce2a57ef9e/aiomisc_pytest-2.0.0.tar.gz", hash = "sha256:71b62b90ee984523c57e18e0757cee540db063947212659c1be9b2199bb54c65", size = 6032, upload-time = "2026-02-09T00:09:06.732Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/9c/552232841a802575d001774ea8dbc551f20567d51bfa1845a866b44029f4/aiomisc_pytest-2.0.0-py3-none-any.whl", hash = "sha256:63a7865ad08430c4d8e7ed6fb8559c434ab205601710eab1f2c39b6cba984581", size = 5304, upload-time = "2026-02-09T00:09:07.931Z" }, ] [[package]] name = "aiormq" version = "7.0.0" source = { editable = "." } default-groups = "all" dependencies = [ { name = "pamqp" }, { name = "yarl" }, ] [package.dev-dependencies] dev = [ { name = "aiomisc-pytest" }, { name = "coverage" }, { name = "coveralls" }, { name = "markdown-pytest" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, { name = "ty" }, ] uvloop = [ { name = "uvloop" }, ] [package.metadata] requires-dist = [ { name = "pamqp", specifier = ">=5,<6" }, { name = "yarl" }, ] [package.metadata.requires-dev] asyncio = [] dev = [ { name = "aiomisc-pytest", specifier = ">=2,<3" }, { name = "coverage", specifier = ">=6.5,<8" }, { name = "coveralls", specifier = ">=3.3.1,<5" }, { name = "markdown-pytest", specifier = ">=0.6.6,<0.7" }, { name = "pytest", specifier = ">=8.4,<10" }, { name = "pytest-cov", specifier = ">=4,<8" }, { name = "ruff" }, { name = "ty", specifier = ">=0.0.84" }, ] uvloop = [{ name = "uvloop", specifier = ">=0.23,<0.24" }] [[package]] name = "aiothreads" version = "1.1.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/5f/a1e9dfe45318b5ee8aeb2be442a7f8b92e18db02f1eff83bbd51292f0c53/aiothreads-1.1.3.tar.gz", hash = "sha256:5ed2682bbccfac4e20d21cd47e972ef39c61b73e612e05a86f4fe5192492f0f2", size = 118636, upload-time = "2026-09-26T08:12:06.752Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/03/fd/436b9c333938963668e0281dd3c872b66df91dcb397d97093d6dc45b91a3/aiothreads-1.1.3-py3-none-any.whl", hash = "sha256:68636da20e01cbd1bc0d9e3de32395d0dc76e94c66fad9af6d753ced1544c093", size = 23387, upload-time = "2026-09-26T08:12:05.495Z" }, ] [[package]] name = "annotated-doc" version = "0.0.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, ] [[package]] name = "certifi" version = "2026.7.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] name = "charset-normalizer" version = "3.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "colorlog" version = "6.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8c/55/ba79756cb90c8d69d599d57785398ac87bba7b19c80e87f4e8a562197c93/colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f", size = 18151, upload-time = "2026-07-23T13:40:40.71Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e", size = 12239, upload-time = "2026-07-23T13:40:39.562Z" }, ] [[package]] name = "coverage" version = "7.16.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/2d/c738872f477f5687152acae68635790387425d407ae37dd3d3a8a6692307/coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210", size = 969651, upload-time = "2026-09-13T19:12:21.422Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/3a/d09495dfd5191b5593852bbe2f037afae768157443378d066d9ecea69a49/coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da", size = 223307, upload-time = "2026-09-13T19:08:44.914Z" }, { url = "https://files.pythonhosted.org/packages/a3/34/310196a80258e28ebf6e5aa814a5a6ffbb18c03cf3c0c0c04191ed893e88/coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07", size = 223817, upload-time = "2026-09-13T19:08:46.738Z" }, { url = "https://files.pythonhosted.org/packages/73/29/ed0fda1fcd440cdfc07e72a07f3c9c3b43f65a6b3d53bff2cba9b9de50e8/coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5", size = 254222, upload-time = "2026-09-13T19:08:48.358Z" }, { url = "https://files.pythonhosted.org/packages/ac/4e/5e2507fbd71ec1047620978f39568b0d2ed6c4468d08495a0c24f5677f65/coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475", size = 256134, upload-time = "2026-09-13T19:08:49.926Z" }, { url = "https://files.pythonhosted.org/packages/7e/b1/a9f97846554bc240473656f9c3874591105f29c08eccf4282daadc6bc281/coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567", size = 258240, upload-time = "2026-09-13T19:08:51.584Z" }, { url = "https://files.pythonhosted.org/packages/a0/c3/2b1ab208d06719394851cdd7dc322157870a93b178410b1bda6cac7755f9/coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e", size = 260202, upload-time = "2026-09-13T19:08:53.204Z" }, { url = "https://files.pythonhosted.org/packages/af/14/ff4db31d6e3126d5b72aab1e868d35ec94e1b71dee46f5c3b54cb1b2438e/coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d", size = 254305, upload-time = "2026-09-13T19:08:54.833Z" }, { url = "https://files.pythonhosted.org/packages/ce/27/c28faa4818f61c49ca3bfb4a77e1c191e79a40447504b89af0bd859cd6fc/coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b", size = 255935, upload-time = "2026-09-13T19:08:56.453Z" }, { url = "https://files.pythonhosted.org/packages/46/5a/42e64e5b716048cae33e2e15f8c6fe04a927467056c533b8e88900da895e/coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf", size = 253995, upload-time = "2026-09-13T19:08:58.159Z" }, { url = "https://files.pythonhosted.org/packages/84/e5/860287acd5a1e29acd337f302b476b54148203590091b1a1555b2e914b50/coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01", size = 257767, upload-time = "2026-09-13T19:08:59.958Z" }, { url = "https://files.pythonhosted.org/packages/a5/c5/4b68006da567b4b413352f891776c35776b3557a0479a28187ab57c21659/coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08", size = 253715, upload-time = "2026-09-13T19:09:01.778Z" }, { url = "https://files.pythonhosted.org/packages/a1/0e/b2d0c47cfbf11770e197f1d0f11a92864eb29fd7eaca45bc9915573d9725/coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572", size = 254624, upload-time = "2026-09-13T19:09:03.376Z" }, { url = "https://files.pythonhosted.org/packages/2e/14/3bab8821b2b942155578fc34e88e21b0670d922f039ab23651f6035ba7f7/coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2", size = 225404, upload-time = "2026-09-13T19:09:05.174Z" }, { url = "https://files.pythonhosted.org/packages/9b/ce/4f4ce667a97d1c538b46a79b89161395ad2beffc248b6c782afc9f521927/coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7", size = 225879, upload-time = "2026-09-13T19:09:06.81Z" }, { url = "https://files.pythonhosted.org/packages/26/17/f3dce5e47351ab34522dd037fd64b111742958e24861bdd2820971bc1a26/coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22", size = 225428, upload-time = "2026-09-13T19:09:08.502Z" }, { url = "https://files.pythonhosted.org/packages/1a/f7/7cd4c9f2a3b7574414222ec425d5eee21cc690d867a013315e3be8ba185c/coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af", size = 223475, upload-time = "2026-09-13T19:09:10.145Z" }, { url = "https://files.pythonhosted.org/packages/3e/f2/9ac65f9cedd43f91e2d3657c11f13aef82aebae89b2243dc9e44fe58a740/coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c", size = 223845, upload-time = "2026-09-13T19:09:11.988Z" }, { url = "https://files.pythonhosted.org/packages/62/4d/f13db452d4367fe1122abfd98ae330596f65a3b40498f8e1a5811f68f123/coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809", size = 255341, upload-time = "2026-09-13T19:09:13.708Z" }, { url = "https://files.pythonhosted.org/packages/5d/6b/85ed86e82835a96ddfbe7705c387c28ce1cbbecd1b6d606f5ddfeeb712df/coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf", size = 258078, upload-time = "2026-09-13T19:09:15.368Z" }, { url = "https://files.pythonhosted.org/packages/cd/d2/f66945853d850b9c05f4e012e37396f1f31d0cd40d062394b229c22e7b56/coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f", size = 259191, upload-time = "2026-09-13T19:09:17.046Z" }, { url = "https://files.pythonhosted.org/packages/77/a1/7b907abe62461f289035c7ac60ab3e6334efb8c425151aac4abfa0c97820/coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360", size = 261452, upload-time = "2026-09-13T19:09:18.756Z" }, { url = "https://files.pythonhosted.org/packages/51/e6/e26f4d6b1069a2a2fee3238a132f85c5b4c1dc25aebd88e015451ff0bd68/coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08", size = 255698, upload-time = "2026-09-13T19:09:20.786Z" }, { url = "https://files.pythonhosted.org/packages/7f/ec/99db7450e813050ebce300e31bbd4f997c76a8c6e3a6d168c5dffc104109/coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941", size = 257111, upload-time = "2026-09-13T19:09:22.693Z" }, { url = "https://files.pythonhosted.org/packages/2e/9d/500df9d3cd8c541ac84b5e0bcab00646be75654e05aa34e8410ec851282d/coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489", size = 255258, upload-time = "2026-09-13T19:09:24.429Z" }, { url = "https://files.pythonhosted.org/packages/c0/24/53318d96da332bb4946f8ac646fa19fd37fecbd0c49144735c716ac69bf0/coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776", size = 259326, upload-time = "2026-09-13T19:09:26.32Z" }, { url = "https://files.pythonhosted.org/packages/c5/ce/abc0462b2e6ae96ae22197d2bc30fe33b6b4e52937e782745436ceb2a761/coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6", size = 254827, upload-time = "2026-09-13T19:09:28.191Z" }, { url = "https://files.pythonhosted.org/packages/8b/e1/0a04eedaaf19196b51f0180968134228c7646e469ed29914e48690a7cf3e/coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0", size = 256698, upload-time = "2026-09-13T19:09:29.922Z" }, { url = "https://files.pythonhosted.org/packages/de/1d/d441a55cf22ce9d8e9e34814806c47441ab844bd534e0f4b64e1c6ae8bd1/coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d", size = 225541, upload-time = "2026-09-13T19:09:31.752Z" }, { url = "https://files.pythonhosted.org/packages/73/27/ec3d032375735dd331477caa051419678079ff90fcb53d0284a6c2bfb757/coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e", size = 226075, upload-time = "2026-09-13T19:09:33.556Z" }, { url = "https://files.pythonhosted.org/packages/94/00/90e9f5c4434878494306b9c0ee8068ebbd829483737d8cefccc58884d728/coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8", size = 225461, upload-time = "2026-09-13T19:09:35.48Z" }, { url = "https://files.pythonhosted.org/packages/aa/74/c08c0c4dc9fa6bcd1d90728a63660aa1b17b488a806948598456c48f75d1/coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9", size = 223501, upload-time = "2026-09-13T19:09:37.276Z" }, { url = "https://files.pythonhosted.org/packages/5b/c8/784986d326663285258a8e39835c46fb730cc85284f0dbcd82078586dd22/coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245", size = 223876, upload-time = "2026-09-13T19:09:39.385Z" }, { url = "https://files.pythonhosted.org/packages/15/76/73fb792928872bbb07e553f920ff55c65ee962c469265feb5d1d4ae5f97a/coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5", size = 254863, upload-time = "2026-09-13T19:09:41.326Z" }, { url = "https://files.pythonhosted.org/packages/ca/8d/1fd78899513244b065ccecdd1cfc6aa8b8f1bdee796d8c4f2aa8e8e5397d/coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5", size = 257460, upload-time = "2026-09-13T19:09:43.086Z" }, { url = "https://files.pythonhosted.org/packages/47/1c/b23ddfcb7ef9bd7b3fdb7be9a5dccf9925ae88e556df7aa5b655283c78f2/coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f", size = 258697, upload-time = "2026-09-13T19:09:45.161Z" }, { url = "https://files.pythonhosted.org/packages/1c/c9/19d0c6ca35778a7e9415c30c46a0c64c9d4374219d004a35563132296896/coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41", size = 260827, upload-time = "2026-09-13T19:09:47.061Z" }, { url = "https://files.pythonhosted.org/packages/97/fc/5862f7344382c62b85df43d483e579168cc062e007f54d895dfa51fe3e7d/coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878", size = 255038, upload-time = "2026-09-13T19:09:48.945Z" }, { url = "https://files.pythonhosted.org/packages/2e/9f/5c26583199a68df8d15124b4590520903f8eb7cef17db293fea712bb0783/coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2", size = 256827, upload-time = "2026-09-13T19:09:50.738Z" }, { url = "https://files.pythonhosted.org/packages/95/13/605198bff079b107336710f28a797312ab132587168600d70a290e7ecd2e/coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080", size = 254794, upload-time = "2026-09-13T19:09:52.577Z" }, { url = "https://files.pythonhosted.org/packages/43/b7/0bbb32dc5ccdac766a13763fdfbffd7d73fc80349a69230c46c6b71e7508/coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037", size = 258947, upload-time = "2026-09-13T19:09:54.372Z" }, { url = "https://files.pythonhosted.org/packages/13/bd/65c31ddd43ff4e61b63721b3dad17cca412dba6e2ce89f92abdf75734339/coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b", size = 254613, upload-time = "2026-09-13T19:09:56.198Z" }, { url = "https://files.pythonhosted.org/packages/25/20/d278115f2ba522b3e3128c6852be265f4e0df7202b2effca4db96cf0217d/coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0", size = 256388, upload-time = "2026-09-13T19:09:58.095Z" }, { url = "https://files.pythonhosted.org/packages/e1/ca/d43aa396fb3a2f71c9411b99d926475f5acaf5c124f605c592865c80c8d5/coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a", size = 225550, upload-time = "2026-09-13T19:09:59.998Z" }, { url = "https://files.pythonhosted.org/packages/0e/30/df2f6114f6a17cffe94721bd1d298f77f86aad493717fa5bc03cb291a1a6/coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa", size = 226091, upload-time = "2026-09-13T19:10:02.331Z" }, { url = "https://files.pythonhosted.org/packages/ee/31/6f90d8aab72a112492bd50e3b5485b53b772b1f5fa70da0a620e723caae6/coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d", size = 225481, upload-time = "2026-09-13T19:10:04.151Z" }, { url = "https://files.pythonhosted.org/packages/8e/b4/2a7c793965bae9f067aabab793a44d7a2f3ee7fb16b01ce1976bbd4a0218/coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15", size = 223546, upload-time = "2026-09-13T19:10:06.019Z" }, { url = "https://files.pythonhosted.org/packages/ef/e2/633469076a2dbbea036cc15a268a3a5d6b2c7dd5d9a9567b2553dfc5ad61/coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57", size = 223881, upload-time = "2026-09-13T19:10:08.246Z" }, { url = "https://files.pythonhosted.org/packages/de/c3/f06150c13284569d53273b909f31222874276a595637b7852571dfeb2c18/coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3", size = 254919, upload-time = "2026-09-13T19:10:10.254Z" }, { url = "https://files.pythonhosted.org/packages/d5/40/47e25b215ae18a29010c8e29be8782a6e04d18ba6224be2bf6cebfce6427/coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc", size = 257428, upload-time = "2026-09-13T19:10:12.124Z" }, { url = "https://files.pythonhosted.org/packages/27/4b/1e2a4267d14cbd12a8489364a9d40020233e6be836d929b363f0e77209e2/coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751", size = 258771, upload-time = "2026-09-13T19:10:14.031Z" }, { url = "https://files.pythonhosted.org/packages/be/2e/9aa6146cea929fab9185bb2642ffef7f47520a6e5efe407f75f9b12f4cf0/coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab", size = 261086, upload-time = "2026-09-13T19:10:16.213Z" }, { url = "https://files.pythonhosted.org/packages/13/3c/f9ad8bcd4fb3d21c9d20a16d6d6c6f999eee8f4498ed7659a3dbd2f4b74a/coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9", size = 254895, upload-time = "2026-09-13T19:10:18.602Z" }, { url = "https://files.pythonhosted.org/packages/b7/d1/47eda9fd1eaeea39fa7b5b13a63b2bed92ab901841fb120b3f9f5e1dc30c/coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412", size = 256783, upload-time = "2026-09-13T19:10:20.778Z" }, { url = "https://files.pythonhosted.org/packages/38/c3/565edf044877cb8cd3373c56885347ffc38f0edfd1f1679a487b208c19a8/coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a", size = 254742, upload-time = "2026-09-13T19:10:22.733Z" }, { url = "https://files.pythonhosted.org/packages/fd/88/87d2b2aeaba719192b2089ff1c2cf89a06cf73a6d2e9f1f145626617700c/coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540", size = 259016, upload-time = "2026-09-13T19:10:24.769Z" }, { url = "https://files.pythonhosted.org/packages/fc/1b/70813185b125768abdcf7899fec4d37edc2e5fc9b60c7045c8f4271ec757/coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9", size = 254559, upload-time = "2026-09-13T19:10:26.781Z" }, { url = "https://files.pythonhosted.org/packages/d8/fa/e7aa5af279aafda633a1ede8bfd7d6916b0c8b2082be86759e0b52e73a61/coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361", size = 256215, upload-time = "2026-09-13T19:10:28.714Z" }, { url = "https://files.pythonhosted.org/packages/38/87/7a894fa4f8c6662d2b6a87a3436950e15b1fa56e01765c9d6634fb2cbeb8/coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b", size = 225719, upload-time = "2026-09-13T19:10:30.743Z" }, { url = "https://files.pythonhosted.org/packages/8b/01/fa7193c8005fb85488f02b0e1cc3c05a233cf2640206dd978af447aeecbf/coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b", size = 226208, upload-time = "2026-09-13T19:10:32.698Z" }, { url = "https://files.pythonhosted.org/packages/da/5c/a08634c714924c3eaef811bb3576c044128aa5e7dfa86c75e52f0761849e/coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f", size = 225633, upload-time = "2026-09-13T19:10:34.79Z" }, { url = "https://files.pythonhosted.org/packages/43/df/ddb8a4c664046b1a0ee29c9c2d25b993e5dbc8fbde715df3694a64532781/coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152", size = 224281, upload-time = "2026-09-13T19:10:36.853Z" }, { url = "https://files.pythonhosted.org/packages/e2/d0/9076e0c762d8afd91182e60a520fa5c92c4a334785eeb9fd6b8ef8fe7e3c/coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb", size = 224547, upload-time = "2026-09-13T19:10:39.359Z" }, { url = "https://files.pythonhosted.org/packages/03/e5/9c59e64b6161704f35fe91549bb19b2bb355e95caf596c26a2065564807c/coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741", size = 265906, upload-time = "2026-09-13T19:10:41.434Z" }, { url = "https://files.pythonhosted.org/packages/57/5a/13ccaffb77f766101bf6f38be9dba9e468b02cc92da4552a57877dbf1c1f/coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1", size = 268023, upload-time = "2026-09-13T19:10:43.533Z" }, { url = "https://files.pythonhosted.org/packages/ad/a1/05cfcf01d3c7c922832698ad46e51d3441d820ce87a943014bb5cf5710dd/coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72", size = 270442, upload-time = "2026-09-13T19:10:45.895Z" }, { url = "https://files.pythonhosted.org/packages/72/15/a2f1544b8e3835d7b769f7dabcc9ac0283e0b646ef3344703ff8f18d83e6/coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8", size = 271565, upload-time = "2026-09-13T19:10:48.123Z" }, { url = "https://files.pythonhosted.org/packages/df/5b/963c2993a82bd313f298d663afe03e164b96ace4d9d4c7561740a559e13d/coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c", size = 264959, upload-time = "2026-09-13T19:10:50.195Z" }, { url = "https://files.pythonhosted.org/packages/12/59/5eba06d1943735d7cd61d46d8c8a20ffe8ddd2da06b3c94366078dadeb9b/coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a", size = 267897, upload-time = "2026-09-13T19:10:52.252Z" }, { url = "https://files.pythonhosted.org/packages/bd/48/af6c30f6ea431bb9b83f9070d268a9cc4fc97490abd32080164177ea999f/coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da", size = 265504, upload-time = "2026-09-13T19:10:54.432Z" }, { url = "https://files.pythonhosted.org/packages/80/f2/6e13852a8656d05fa83284567dd5a5b1e6d89bef79fe3effca2787159eab/coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2", size = 269235, upload-time = "2026-09-13T19:10:56.563Z" }, { url = "https://files.pythonhosted.org/packages/c2/32/b4fe465daa64ece674f83a750dfa4ba0fa3c5c74d6ef5dbb8dfce892cf0d/coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e", size = 264347, upload-time = "2026-09-13T19:10:58.634Z" }, { url = "https://files.pythonhosted.org/packages/54/f3/88b5c0e4ca3994c6d5feb7b1bf4c9a62cee205553159184968426930a7b1/coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae", size = 266660, upload-time = "2026-09-13T19:11:00.746Z" }, { url = "https://files.pythonhosted.org/packages/97/72/6eff5456d7ba7f1c4678af531c33f9d957cae3201bd229b056fd13a204a3/coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2", size = 226026, upload-time = "2026-09-13T19:11:02.846Z" }, { url = "https://files.pythonhosted.org/packages/8e/c8/6e5ae3d8d4d0f2c0078985bf4db55fafd90e8107b1bf91ee3547a13f5694/coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d", size = 226862, upload-time = "2026-09-13T19:11:04.921Z" }, { url = "https://files.pythonhosted.org/packages/be/c7/68f9f0734afc904a92b974b489545b6a15700f3b1c4bd36eae764561e661/coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6", size = 226171, upload-time = "2026-09-13T19:11:06.969Z" }, { url = "https://files.pythonhosted.org/packages/ae/16/e11addf5322d98e86307da9e00c94644b97cb72c534c74cda89705bebeef/coverage-7.16.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743", size = 223544, upload-time = "2026-09-13T19:11:09.108Z" }, { url = "https://files.pythonhosted.org/packages/92/d9/7ccd6484f615961d94b09c75904f87d86375109596069ae3a495a205dbbe/coverage-7.16.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e", size = 223882, upload-time = "2026-09-13T19:11:11.132Z" }, { url = "https://files.pythonhosted.org/packages/59/37/2fd78152a557df4a7e4ad78d6851ede90c211838d526e4916c6016f45144/coverage-7.16.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae", size = 254987, upload-time = "2026-09-13T19:11:13.735Z" }, { url = "https://files.pythonhosted.org/packages/7a/4a/58e2f9b8b13cc422aa7f474a498945ec7f446ef473b5b6bae18d2981f2a9/coverage-7.16.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370", size = 257902, upload-time = "2026-09-13T19:11:16.025Z" }, { url = "https://files.pythonhosted.org/packages/3c/74/63f8f6a67c03b86455c726be273b2aa5b57fd1122f22350d141bbb142e91/coverage-7.16.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae", size = 259522, upload-time = "2026-09-13T19:11:18.206Z" }, { url = "https://files.pythonhosted.org/packages/e2/3b/1f2261da7483e345fe55de11e41ffc16a543511e37c0912134c0dccc4f6c/coverage-7.16.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de", size = 261723, upload-time = "2026-09-13T19:11:20.628Z" }, { url = "https://files.pythonhosted.org/packages/a7/8a/7eb1a361e044b7293f49c921a974b532627ed37f3bef5e7e9f58bcd0d5a2/coverage-7.16.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1", size = 255462, upload-time = "2026-09-13T19:11:22.733Z" }, { url = "https://files.pythonhosted.org/packages/67/66/6d94f7ea87e99c519def5cfa5d4a1ab20247d6b403eb6e6b5b63d6aa5985/coverage-7.16.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c", size = 257617, upload-time = "2026-09-13T19:11:24.933Z" }, { url = "https://files.pythonhosted.org/packages/3d/52/c3de0a3868589a1463a4f1cb0222eca0e5fa9f91da865ccf20d2e19ec19c/coverage-7.16.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6", size = 255495, upload-time = "2026-09-13T19:11:27.158Z" }, { url = "https://files.pythonhosted.org/packages/9e/a3/ea10e75f20fc1e7be10b166a494826a965d19b1dc9c8a81c511a01153311/coverage-7.16.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f", size = 259728, upload-time = "2026-09-13T19:11:29.33Z" }, { url = "https://files.pythonhosted.org/packages/0e/96/9d8020d97de46f0390739beda196322586cdd87e1fc357223e6a28bc4135/coverage-7.16.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df", size = 254904, upload-time = "2026-09-13T19:11:31.86Z" }, { url = "https://files.pythonhosted.org/packages/5b/21/2ff8867d4560f4f639a82d8fcf13c27eea88a96193daba096027c48bb929/coverage-7.16.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040", size = 256828, upload-time = "2026-09-13T19:11:34.078Z" }, { url = "https://files.pythonhosted.org/packages/6a/4b/feacad51cb5163e3baa13281f8a3098f15d7934cc1f28cfe656557eb4e46/coverage-7.16.1-cp315-cp315-win32.whl", hash = "sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827", size = 225722, upload-time = "2026-09-13T19:11:36.314Z" }, { url = "https://files.pythonhosted.org/packages/39/34/66bbc6ffd3c51fa67604c566920e772c5e3baa57f88dcf7b109c7f18b2cb/coverage-7.16.1-cp315-cp315-win_amd64.whl", hash = "sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d", size = 226195, upload-time = "2026-09-13T19:11:38.493Z" }, { url = "https://files.pythonhosted.org/packages/f7/96/4bad920d4caed127c37c136a64b0730fa495517d2eb58809e0d81e6e7dff/coverage-7.16.1-cp315-cp315-win_arm64.whl", hash = "sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855", size = 225625, upload-time = "2026-09-13T19:11:40.664Z" }, { url = "https://files.pythonhosted.org/packages/f5/2b/a88c7906eeb7da4394e932060d7008e153b3a2cd8d11782d82faa03bb7a3/coverage-7.16.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6", size = 224271, upload-time = "2026-09-13T19:11:42.876Z" }, { url = "https://files.pythonhosted.org/packages/1f/1f/98db59c595680d16f553890deac6d72610e04f192c1964ec9b69cbe790ab/coverage-7.16.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d", size = 224564, upload-time = "2026-09-13T19:11:45.071Z" }, { url = "https://files.pythonhosted.org/packages/b5/e8/699e236d4cb97ec282b0655afc85acf12349e71a02fff0f1a7d0fb643079/coverage-7.16.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6", size = 265423, upload-time = "2026-09-13T19:11:47.666Z" }, { url = "https://files.pythonhosted.org/packages/96/1b/6eaa21912863b3e3bf23cfe605fb62929bb1a41dc33d2bfa8e92000232bd/coverage-7.16.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1", size = 268503, upload-time = "2026-09-13T19:11:50.12Z" }, { url = "https://files.pythonhosted.org/packages/9f/ac/4eb46bffa98c28a80559a58c12f17e19308d52b31174af1bd49decf76606/coverage-7.16.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1", size = 271059, upload-time = "2026-09-13T19:11:52.363Z" }, { url = "https://files.pythonhosted.org/packages/ed/d4/17a51ef1f6a084c9abbead522cfbee0fe9d29a593128781e724b7a0795b3/coverage-7.16.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b", size = 272039, upload-time = "2026-09-13T19:11:54.935Z" }, { url = "https://files.pythonhosted.org/packages/4a/ff/9fdeba8b8aa0848030f45f869ddedc306795a8c6e53e901b8553afbf5415/coverage-7.16.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de", size = 265869, upload-time = "2026-09-13T19:11:57.423Z" }, { url = "https://files.pythonhosted.org/packages/95/eb/ab3eb506b2e4dd278440fbbbc7ed18424a86d8a981db0b90fdba66f5d34d/coverage-7.16.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef", size = 268883, upload-time = "2026-09-13T19:11:59.71Z" }, { url = "https://files.pythonhosted.org/packages/d5/0c/f4edefdce33f01954a74237df5ee83ccef5dcd1165e04abf0adf47543fca/coverage-7.16.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876", size = 265359, upload-time = "2026-09-13T19:12:01.953Z" }, { url = "https://files.pythonhosted.org/packages/df/86/ce5ed885fc7ce2adc9a5971134ace257d0f84f19c3d117da89c179b9662e/coverage-7.16.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8", size = 270056, upload-time = "2026-09-13T19:12:04.46Z" }, { url = "https://files.pythonhosted.org/packages/48/a1/5dbd5f95070cece25c6df9e6ea4ded2a87dc695ce82a49bcb2116452b342/coverage-7.16.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220", size = 265498, upload-time = "2026-09-13T19:12:06.811Z" }, { url = "https://files.pythonhosted.org/packages/8e/ff/254b26f3300f87c7c53ac97da41ae75f0e2ac066aa964bc073c0f3750b38/coverage-7.16.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0", size = 267459, upload-time = "2026-09-13T19:12:09.479Z" }, { url = "https://files.pythonhosted.org/packages/61/20/bf9958f8ddbbf6a2d39da3d4009f33e0fcf19c2d2fd99715c6494cdc8e8c/coverage-7.16.1-cp315-cp315t-win32.whl", hash = "sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5", size = 226021, upload-time = "2026-09-13T19:12:11.933Z" }, { url = "https://files.pythonhosted.org/packages/35/8f/12d46948704d9e437c8dca2718e5d54ecc2f85396b64b500c29d8dbe10db/coverage-7.16.1-cp315-cp315t-win_amd64.whl", hash = "sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db", size = 226853, upload-time = "2026-09-13T19:12:14.254Z" }, { url = "https://files.pythonhosted.org/packages/a5/7f/1ca5fd0601054fce5a3539375b997e67683646e2bf62f60c41e70b529fcc/coverage-7.16.1-cp315-cp315t-win_arm64.whl", hash = "sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0", size = 226163, upload-time = "2026-09-13T19:12:16.589Z" }, { url = "https://files.pythonhosted.org/packages/96/1a/d6d16babd0a5fe4c3fae40702158c570351694e74516d8d81b86c5637448/coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4", size = 215264, upload-time = "2026-09-13T19:12:18.895Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "coveralls" version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "requests" }, { name = "typer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/16/aa263cb5450de470234f5176ce42664c5e0535aab1a2094b739447065797/coveralls-4.1.0.tar.gz", hash = "sha256:dab364025ba80cbb95ce56c6fc62cd9172d7fd637060ea235dde99d9b46a4494", size = 12736, upload-time = "2026-02-28T15:16:47.591Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/60/f1/5a884d2e8e3e7e7d854d3032b777bd22aad1598161cf9aa77ac37d684908/coveralls-4.1.0-py3-none-any.whl", hash = "sha256:bfacfda2d443c24fc90d67035027cec15015fff2dbd036427e8bf8f4953dda2e", size = 14168, upload-time = "2026-02-28T15:16:46.181Z" }, ] [[package]] name = "idna" version = "3.20" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "logging-journald" version = "0.6.12" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/71/df/c013c3d8f37a351ceb35f166bc9a0a69365655ab13eb4366032e96c6da33/logging_journald-0.6.12.tar.gz", hash = "sha256:d6bd338460b5ecd88f6b3e9a6499a6994c65d5d8296f3aa4830b25a7d7e815a9", size = 7747, upload-time = "2026-07-30T20:31:27.609Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/24/f3/2b2033e02f6a08eaefed14351056b797f780c7f5aaa6765e66d734fac91b/logging_journald-0.6.12-py3-none-any.whl", hash = "sha256:b94d1c7082d7a7ddc69f8fc1a6cf420da3d97a03363c12659859fec09f44f811", size = 7080, upload-time = "2026-07-30T20:31:26.517Z" }, ] [[package]] name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] name = "markdown-pytest" version = "0.6.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3c/f7/5d9de5ba944c289e0a41bb881e711f3656d2c22b626c2452a863c6f35908/markdown_pytest-0.6.6.tar.gz", hash = "sha256:903c3d4b2439396d06019768ad0eb7a4a230d4381272e22fa241e86fb223493b", size = 110498, upload-time = "2026-08-07T09:05:08.913Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/91/87cf4e15debb7ac0b1b57dda2caa111bf1695c68a9efa952d814f71290a9/markdown_pytest-0.6.6-py3-none-any.whl", hash = "sha256:83afab0568d72b7f57b9c62837dd784c1e4966a6c6ba9cc0eaf7cc80443d2e60", size = 16084, upload-time = "2026-08-07T09:05:07.827Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "multidict" version = "7.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c4/64/642465a4827331a98ba4ae29f97658be25d1bdcb868872a2f78f7482b507/multidict-7.0.0.tar.gz", hash = "sha256:a7fcd089a0af2e0ef053c0d39c22c9ebf2434dddcb91034fd2f59ec99623788e", size = 280561, upload-time = "2026-09-26T19:36:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/e5/b3bd09c9f46182d34d655e0797da1294c7ce3d7cb35a10f4e35691f396ae/multidict-7.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:091568698da91aaabe3e5cb9d5884f1a950f274cc39bdc2191f6462baccaa109", size = 145886, upload-time = "2026-09-26T19:31:23.76Z" }, { url = "https://files.pythonhosted.org/packages/e3/03/8dbe4eed699591ec6de33721b0412ffa25636f9a9099b8436ef3419e342b/multidict-7.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3900adeaccb518ab36c805ff4cc8bcb0439525ac7c3ad10caecbc6a35dd1cc7c", size = 86830, upload-time = "2026-09-26T19:31:25.063Z" }, { url = "https://files.pythonhosted.org/packages/6f/61/c223f9f1199da786c07257fde960d4ae3af595ed7f0d385f67b207a4e96a/multidict-7.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc68c8e839207df4d2724a301d14ab5f7dcd03ff580c5457c838c72bcd2e916a", size = 86675, upload-time = "2026-09-26T19:31:26.446Z" }, { url = "https://files.pythonhosted.org/packages/d3/55/1e14f33459f82135e1d40acbab8ba861a048dcc68137dcb367ea796b0d9b/multidict-7.0.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4ea9e667503316c02c9624b0c5c7f738c139eca1b952250fe7fae0b482f65d9e", size = 485162, upload-time = "2026-09-26T19:31:27.705Z" }, { url = "https://files.pythonhosted.org/packages/e1/0b/1f941d8789a4320e756ae0df096cae60755e94d76d62c3fe9654c0f57a1f/multidict-7.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96533d74d4ace946dc88685dd2eeedc40142b516d43fd3d2863896d3af9aae5d", size = 488133, upload-time = "2026-09-26T19:31:29.348Z" }, { url = "https://files.pythonhosted.org/packages/96/8e/640a2ef76614add4e1f9ec63c2d42715e21aee994a235792f24198cb3415/multidict-7.0.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7f8af0d37d80023375007a4485131e2b70553570f82b7fc5cc50b505f798dbca", size = 448743, upload-time = "2026-09-26T19:31:31.011Z" }, { url = "https://files.pythonhosted.org/packages/26/e1/59a497744cb032e517e5b9cd0422fcc8f084be3b168c2a67d6a66fbbf61e/multidict-7.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca8b14b9262894905ab2f724bfcdfb28f3fa0d09b7b970878390686b385e471d", size = 503969, upload-time = "2026-09-26T19:31:32.521Z" }, { url = "https://files.pythonhosted.org/packages/b8/94/36d3df4e8cddd8c516279570906c57aedcca636f68531f6217ba9f34b0b4/multidict-7.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1556cbbdeef23e6c89cdbddefffa3ae099c117ffa2ae5f648ef62633d0da655a", size = 517168, upload-time = "2026-09-26T19:31:33.973Z" }, { url = "https://files.pythonhosted.org/packages/ec/b7/951633d47e0ca0a1a6314a4bd612d4f0fff3ed8c85809ea9936d66f3e66a/multidict-7.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3816feef94ff66a282daed56c98c8648af9b7780a99a2eeb428999ade51bc968", size = 498997, upload-time = "2026-09-26T19:31:35.426Z" }, { url = "https://files.pythonhosted.org/packages/fc/1e/e84844f4c6eaca36339fdafaee4ba6e93b5eb0ea2685a026f95c111adf43/multidict-7.0.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ded8237a0fc9617ab05372c0a025cf63907ae24d6366f9a39c965d8306952c2", size = 442689, upload-time = "2026-09-26T19:31:37.11Z" }, { url = "https://files.pythonhosted.org/packages/92/e6/efe8df6611b759d85ef578f9e7ec46b67b426c5c0b0b82608d637d6e4ab1/multidict-7.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9697821e17ad7ace5689e546c9dd976e40c43921926c54bcc8ba7b1ebffc616a", size = 478850, upload-time = "2026-09-26T19:31:38.537Z" }, { url = "https://files.pythonhosted.org/packages/1c/41/eff622f3246b18fe28359c53c5c0fba4aa8d439844b726a067fd944af011/multidict-7.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:998c1955633ba0f5a6f695d8dcd53c73c7869c0328a40c1b0d901c2f34321580", size = 455227, upload-time = "2026-09-26T19:31:39.882Z" }, { url = "https://files.pythonhosted.org/packages/4e/a6/df33da1fb6950b53e4d7c4ff02fc25ca20f0385db32bd49c6fbf3e9bb9e3/multidict-7.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9768378aecc054c8739646b7134b0dda9b68125f7dd1e99f0146eaeac31e4d5a", size = 488226, upload-time = "2026-09-26T19:31:41.396Z" }, { url = "https://files.pythonhosted.org/packages/25/d1/0a7237fa703c22c2076b7f6c72bdb26e441f3a4832bcd2a0fa5f7c4976cc/multidict-7.0.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:64912716498343f51140c0102776d5c190d8964cf6aa251973e18eae273bcf38", size = 492252, upload-time = "2026-09-26T19:31:42.843Z" }, { url = "https://files.pythonhosted.org/packages/f0/95/abf206c4fb929edd7f6d1a344e681092879d0f8dbd621568964245caa849/multidict-7.0.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b9b2cc7c01e3745f6316c4d1d6227b387003afb1faccfb370ac133f8f11b33ab", size = 440480, upload-time = "2026-09-26T19:31:44.593Z" }, { url = "https://files.pythonhosted.org/packages/ee/32/9aefd96253f685054bd12ed527df049eaf0bbd17f22c24ed8966d63b0db7/multidict-7.0.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a9a4fa294cfab22dfe8ff5812ace4a013b49cec9a1108776e75d939185e434c2", size = 499262, upload-time = "2026-09-26T19:31:46.144Z" }, { url = "https://files.pythonhosted.org/packages/20/94/a3e818f2dc60a86de7a2325365153b7784656c1f60e5e1e34e5981647d01/multidict-7.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d7d7f5ffc78a1ced2d8b50ae44be8820f4ded6a07c1c8dbd00325a55add081d", size = 494877, upload-time = "2026-09-26T19:31:47.896Z" }, { url = "https://files.pythonhosted.org/packages/be/44/f2fd2a31fc6d6f7c5f61fa717ef8bb320b61fe0d432a02c4ab2c9c748d26/multidict-7.0.0-cp311-cp311-win32.whl", hash = "sha256:dbef0f189e11b144058edc02f4f670f1e9279bbc1a96ba33a47641b987017f01", size = 75799, upload-time = "2026-09-26T19:31:49.296Z" }, { url = "https://files.pythonhosted.org/packages/93/dc/d086425a0d006332224ad3d3b716a1db022c141b58be09e73200781c2d16/multidict-7.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:06dd7644b8d597c39bd0b505b7c824b6439e75a2cb9d7d2b1cf9fe48434e2a1c", size = 87103, upload-time = "2026-09-26T19:31:50.803Z" }, { url = "https://files.pythonhosted.org/packages/b2/60/6896ae30e5e71e94cb9bcb669d68549055839422c037411d03779aeb1beb/multidict-7.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:661d3c9a8be6168dd528c774ea8f1922047c5bd66bf7a9447e74fd371e25fe3f", size = 84718, upload-time = "2026-09-26T19:31:52.08Z" }, { url = "https://files.pythonhosted.org/packages/63/b0/61a4ee75e05e36d7d125aa84931f12b6486db1af6a543e7e10a73e239330/multidict-7.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2cd130baaaebe33ed8f0a47e8c3300711cedafa87bf1daa7a34a13193c474c3", size = 148129, upload-time = "2026-09-26T19:31:53.323Z" }, { url = "https://files.pythonhosted.org/packages/a1/15/399f284a99c297e1163790a3766cb2a01276c1f90adc91e723c28561df33/multidict-7.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e535a7e058f4f05b8f04c24e8b63aa2f026bb7af8e7a565e8676c887f16910a6", size = 88465, upload-time = "2026-09-26T19:31:54.721Z" }, { url = "https://files.pythonhosted.org/packages/8e/ce/20b2623af4a440137c909536f927862a053f0352cee7cbad3cd9901a2cce/multidict-7.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af98eb32a76f2a8fbd6c81ef08a7f4c06f2c092d8229e4d7138181469a216154", size = 87425, upload-time = "2026-09-26T19:31:56.191Z" }, { url = "https://files.pythonhosted.org/packages/c1/42/2355cb4bcbdf4b799b43b22972313cf1c34947995446bd5e052294599a6f/multidict-7.0.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0b88ea63284a46b3daf97f7b4a419bcc948a6c3c3b281fe797846ad03bc25718", size = 489839, upload-time = "2026-09-26T19:31:57.471Z" }, { url = "https://files.pythonhosted.org/packages/ce/00/f362f240a4db760847fdafd5cb7f0c873e9733971b163fb0030a7e0c565e/multidict-7.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0c7cd0378c5362cc75315e479d9c9de4e64ede401909a195ae3b8572db26d3a", size = 498731, upload-time = "2026-09-26T19:31:59.307Z" }, { url = "https://files.pythonhosted.org/packages/c9/df/a5b258bb521b65774ea159d3e37220c6678eba4b127069ef4936b4982c61/multidict-7.0.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:30e2ab302e861f36c37b6f9167d5b0cda3f215a1af3114095247b5fc94774739", size = 460243, upload-time = "2026-09-26T19:32:00.888Z" }, { url = "https://files.pythonhosted.org/packages/74/d1/df90613815710793dc4a2e333353af38944f59ac144154a8ca6329089ce0/multidict-7.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ca4648e8b65449a5c854055aed36f0019103d01e8d2682d3f26ace2822b6238", size = 515934, upload-time = "2026-09-26T19:32:02.317Z" }, { url = "https://files.pythonhosted.org/packages/c7/1f/695ed5d362df8c9b5538102c3ce26c2c78f5fc4b0c582a91880eb56c0ad3/multidict-7.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdaece95e0f0ae315d04f6bfa76541656db19c56428163b54b8c4272ebce9bef", size = 530471, upload-time = "2026-09-26T19:32:03.761Z" }, { url = "https://files.pythonhosted.org/packages/a3/5a/a1a031791be14679522a4d23098600dd2b6f1be5cc17179f58fb56ec40a4/multidict-7.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:021bf646def71ddfc234d886f80df2cfe5cb57339ee9537ca5d6402c5457079f", size = 504443, upload-time = "2026-09-26T19:32:05.35Z" }, { url = "https://files.pythonhosted.org/packages/f1/c2/f3c2736b49ebd516e0aa7d8483f97c6bb58cc56b046244d7cc8de810ac32/multidict-7.0.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eab81025dd4091f8185a42243d76463c82616adec279fdbea1fdbad194c155e4", size = 449378, upload-time = "2026-09-26T19:32:06.807Z" }, { url = "https://files.pythonhosted.org/packages/98/6f/d0f23d7011864e252a81cda6648dd32383221f4593f18a3403f7c9bb60bc/multidict-7.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:39c7c56235010b4c6c982891a0f1f028202872b6b4354df2c5692d31b3489abe", size = 487311, upload-time = "2026-09-26T19:32:08.65Z" }, { url = "https://files.pythonhosted.org/packages/1e/a3/9668ef115c4da204b00a95e080fc9ad549d65f44e915b15b01bd6fb04c59/multidict-7.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ead5d89c2898c372abfdb6d85802a11700fa9c790e5e27c45ee69562a123973c", size = 472507, upload-time = "2026-09-26T19:32:10.302Z" }, { url = "https://files.pythonhosted.org/packages/27/73/82bb3b1924d7ad0c88aa3f89d4a5f52b244ada1c818224267684fe9e8e9f/multidict-7.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd3760423eaf9e3c7292a010663e305fb9fa737f5dcb328a39bebb34d9f59bef", size = 491513, upload-time = "2026-09-26T19:32:11.721Z" }, { url = "https://files.pythonhosted.org/packages/57/43/b612efe617fb9519c845c59425543fa951c2ebcafe5ebdf2656749be39dc/multidict-7.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:106f4e1db3f450bf9fe3fc240076982b6d500a6b8283c019f88162893b4781ff", size = 504855, upload-time = "2026-09-26T19:32:13.412Z" }, { url = "https://files.pythonhosted.org/packages/37/e4/ef4703a04c6a62756638a5dac086286b053176e59381c3b8d4dd3f283df3/multidict-7.0.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3bc8089575eed9f89e9244abf3a0aeb17b2c69108292c5efea55501284fbca03", size = 445363, upload-time = "2026-09-26T19:32:15.189Z" }, { url = "https://files.pythonhosted.org/packages/34/f8/87b9956daca873d0ddd2eed631177693b2e74dc365c44b7e13d0038e5b15/multidict-7.0.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c915975dba1775dbf3ce99bc45f32d2e792b0c93437ff1ac451485d93e10d775", size = 512499, upload-time = "2026-09-26T19:32:16.919Z" }, { url = "https://files.pythonhosted.org/packages/3d/aa/b2efeca4d17f64e5214fb99d7b159540387557125ed03019d207cd33473e/multidict-7.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:70add0fc41f75533f082320a72a8e101241a0907ed07ffd35b35a8d447bf688c", size = 499463, upload-time = "2026-09-26T19:32:18.495Z" }, { url = "https://files.pythonhosted.org/packages/98/1a/7ed3360b51ff3d7d7cc4685ba559193c35b2ba2b7c9d9c982fe016d01467/multidict-7.0.0-cp312-cp312-win32.whl", hash = "sha256:1ce6e20ba26ac5f053c9bae1a628079e9040948f0e0711ab130b90d273ad697c", size = 76679, upload-time = "2026-09-26T19:32:20.126Z" }, { url = "https://files.pythonhosted.org/packages/3d/58/b100dc9169c4f3bad9b6b3c520a2a47ef21d5d9120f11ff3c7dbca1eaa10/multidict-7.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f39d891905c19243918ce86ef8d0402b6e4585ef023542efa1f628ae0e4056d2", size = 87219, upload-time = "2026-09-26T19:32:21.54Z" }, { url = "https://files.pythonhosted.org/packages/f7/21/9f37e8342a485e47aa15430ed7388950c4ecc2fe701dcb2cf40704abae6b/multidict-7.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:b2d3ab0cc8c74b21020ce614caaf1e8ea970c4777af3a52df8ce66b638912e7b", size = 85721, upload-time = "2026-09-26T19:32:22.954Z" }, { url = "https://files.pythonhosted.org/packages/5c/6e/c388fccb88dd6cb41ce2454e420c869fb0629ce10bfe12e4cad578ba940d/multidict-7.0.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:15d9ad70615ce1b37c124445145f071ba4eaa88cb21b49a6b81727cc85aefc37", size = 92310, upload-time = "2026-09-26T19:32:24.366Z" }, { url = "https://files.pythonhosted.org/packages/58/2f/33505a5c498613d53ab1d87b3c9425f6757afc7e48c2409f7aa001d0cdc1/multidict-7.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:313e336c75a53447cd943d93235c4702428b9740cb681e7174e398ba1f3f2918", size = 85114, upload-time = "2026-09-26T19:32:25.975Z" }, { url = "https://files.pythonhosted.org/packages/76/f5/27b19597821cb80d082b649e5e6e01033aed5e0eebdbbc6a644eb49515a7/multidict-7.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a4838959217e2652ef7dce1d01bb370aed3e5b694eb7a8258fa1cc50beef08ca", size = 86952, upload-time = "2026-09-26T19:32:27.429Z" }, { url = "https://files.pythonhosted.org/packages/f7/58/a5e44d5453c8bb295979d242cfd3beee06f6ab9f6c1c06a13c2191b792b1/multidict-7.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bd6520538e19fe104c3b74cc64767d6f8959dd2c9b8d59da62d83641ea646deb", size = 147356, upload-time = "2026-09-26T19:32:28.816Z" }, { url = "https://files.pythonhosted.org/packages/a2/c0/4da412a7267ac85759300a078f3bdad986e18a30c1ead8b74d2e3b335a2b/multidict-7.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e9bf055becb08ff797d5cf80b760b726b8babb39046564d2b8481bbe6504bce", size = 88022, upload-time = "2026-09-26T19:32:30.44Z" }, { url = "https://files.pythonhosted.org/packages/b6/46/bebeb7df10298d390942e61bc7fc48472183ee4f5a0b7c285d93369b6874/multidict-7.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8e443cab381f8b6d24950982ae508600d464412cbdcf78e263cf8cd9b8372d68", size = 87046, upload-time = "2026-09-26T19:32:31.881Z" }, { url = "https://files.pythonhosted.org/packages/ab/d3/38b254c21cb8e9a65f8751ad8ba0a6f9baad75e3b6d3e559d1d05db26482/multidict-7.0.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e45bb7b9c850f216c0081dab8c6862fdec6b8be1c800b2a1aa4d1e56dcfacb54", size = 488729, upload-time = "2026-09-26T19:32:33.338Z" }, { url = "https://files.pythonhosted.org/packages/2a/28/19d66c476cc44fffdeacd5227a59046f94612075008069286bdbb9d3fa35/multidict-7.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2f742000b47d806e5798f5ef0cdb2ed43d822fc913331a33ed1a40515495a7", size = 496708, upload-time = "2026-09-26T19:32:35.286Z" }, { url = "https://files.pythonhosted.org/packages/0d/66/3bb2d2cdf67742b002985906785b0db1584ca2e67ddb3677f6e9614321d6/multidict-7.0.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70e28954ae5ae3c1806aa6d85bbb26ef7ab73017228eb236a3f245e24720bb34", size = 458261, upload-time = "2026-09-26T19:32:37.027Z" }, { url = "https://files.pythonhosted.org/packages/af/5c/c8ca15c191f957e8d86ec65148ed1e8ca189f456e66153e23c4ae610649a/multidict-7.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c41ccfa34b152146ccf05e8da753a85f8e67437477f8a00783aa427a132abd31", size = 511932, upload-time = "2026-09-26T19:32:39.025Z" }, { url = "https://files.pythonhosted.org/packages/14/ff/0665b2c3d5e42165a96fffc4c34c3d5fa66d0819d82b9bc2977b9a6aa1ed/multidict-7.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9a1de5f1da3ae056b45b6599a53dba6c86879bdfa5c3f15d7dde99f6cfbc03c6", size = 529152, upload-time = "2026-09-26T19:32:41.068Z" }, { url = "https://files.pythonhosted.org/packages/ab/f7/967875e5d1f908613cfcd5d9d078011168acb88a128028b81a0de43e8aa6/multidict-7.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:094852ebc620a10ee0e11f36a68e016d73596c0f5ed8351a9a52fcff7d449ad6", size = 506337, upload-time = "2026-09-26T19:32:42.839Z" }, { url = "https://files.pythonhosted.org/packages/a8/f9/56f04cf560bf736c23ba9cacc1ae3fcd2bbb591ba4f166c4bc08f5fead61/multidict-7.0.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03cc4027103bb61feac20336a15fced1a6a369fed6a61c516353921207e4578c", size = 448144, upload-time = "2026-09-26T19:32:44.556Z" }, { url = "https://files.pythonhosted.org/packages/07/ea/ff7978bb38f2e2908cf7fc80061175cbfacdcada248dc48fb79a10c0e92b/multidict-7.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0fef8622896443fd0bdf39586391c2162ac05004ba4c9918827dfd2f79da750c", size = 485535, upload-time = "2026-09-26T19:32:46.439Z" }, { url = "https://files.pythonhosted.org/packages/ed/6c/1e9a6457587da0a31b3ef31ca0a40624877ca29408c8a7385260a3d45cd9/multidict-7.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1847d33ca198aba3bf5fad036a7e0376cca325e7ec89f26362f9aa48994d8146", size = 471078, upload-time = "2026-09-26T19:32:48.166Z" }, { url = "https://files.pythonhosted.org/packages/ea/12/ee11505f0ae23a344544010485db982290c0e6105caefd74ce3830c315ca/multidict-7.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b5c93f582f8a973ea803c46097729e34969683bc99f99fc00321b1f76317e81", size = 490764, upload-time = "2026-09-26T19:32:49.861Z" }, { url = "https://files.pythonhosted.org/packages/b9/8d/1e954f7cbb1f2080c2712b6a4cb6ce196037c06c686ae030003465b65ad7/multidict-7.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7c717376f801dd1ee102b292e0a4ca6037e948a8710316e64e698fe39eeb1307", size = 500752, upload-time = "2026-09-26T19:32:51.756Z" }, { url = "https://files.pythonhosted.org/packages/da/54/0f3899f51364f502219192b8c03da123c8618e9709499194ecd8e4143cc9/multidict-7.0.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c6d6e73b9f7b70558de5624b036815464a7ead4671fe2c3a4b3ce444c240cba6", size = 444706, upload-time = "2026-09-26T19:32:53.515Z" }, { url = "https://files.pythonhosted.org/packages/54/03/8edc3cb05a35185a4910582a50aa4dd342cceda3b26b2053dbd004adc9e8/multidict-7.0.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:18f627c69d041ae17d8fb45dbc63e192a2afc3ba062195441b5a50e93c50ae68", size = 511149, upload-time = "2026-09-26T19:32:55.281Z" }, { url = "https://files.pythonhosted.org/packages/d1/b4/4234b76f97b2966d1de2f3507f5dc9af4072e2ed16cfb476855490e34ae9/multidict-7.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8477e14353097077e1011464794d11e1d7559e821ab7c612cf7c06e7826ee1ba", size = 501451, upload-time = "2026-09-26T19:32:57.073Z" }, { url = "https://files.pythonhosted.org/packages/d8/a9/fe7965b74cb3cf822d3e88063f13e3eff2600c36022f69c603b5b52f3e21/multidict-7.0.0-cp313-cp313-win32.whl", hash = "sha256:2efca6de4fbefc12f2aaac2d5e1c176a3864d22250246b44fe3823e3634f06f8", size = 76414, upload-time = "2026-09-26T19:32:58.84Z" }, { url = "https://files.pythonhosted.org/packages/e9/c7/a4960e81ba8a3a9698011d1ac92d4d11ce8c9bb0ed6d235de76c49cae3d1/multidict-7.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:188a7f89a1935a138237088bff22400bacd25d80fb0d3a8a26fab25961df2239", size = 87005, upload-time = "2026-09-26T19:33:00.576Z" }, { url = "https://files.pythonhosted.org/packages/6c/66/7f6b0898c895f2b39ebaa3fc34d32537d263aa5c74809ee747b5e705da80/multidict-7.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:debb701a0436c96f2717c6156428a2c8bce2559236e120a674d503aeed678aa3", size = 85466, upload-time = "2026-09-26T19:33:02.072Z" }, { url = "https://files.pythonhosted.org/packages/45/89/9736c32a9ad36279e72e751e8003b5dbc801bd6154b12118fb94d9c29494/multidict-7.0.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:da9cd9734443b41f81d7b0cd311bac7802d6764b69025d710f998c249b9648a3", size = 91521, upload-time = "2026-09-26T19:33:03.72Z" }, { url = "https://files.pythonhosted.org/packages/01/d8/d494180a49f06118a8a48fec880c912402acd77a629b20bc79f855064ef4/multidict-7.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2a3d428470748cd630997f0bb5a2b2d4000a232cf573d5ede3f4e35513ba2558", size = 84507, upload-time = "2026-09-26T19:33:05.296Z" }, { url = "https://files.pythonhosted.org/packages/66/3f/400a6d5f1f4f8c25d07462cd54cb6396173f3c79c32510fb50c1deaef0b7/multidict-7.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f8266554a44a771e63ccd622aa8f7d2db4e297c6405d0d77131707359dcaa70f", size = 86316, upload-time = "2026-09-26T19:33:06.781Z" }, { url = "https://files.pythonhosted.org/packages/37/9e/f9b8e7fe274737b5e39cf0b0ef449063b8485271ff1cb6164514d41eaa8c/multidict-7.0.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e21a977572daab253274212a31c7d8a796c1ecafe2b32eeb90b53c4df88bdbd4", size = 145979, upload-time = "2026-09-26T19:33:08.505Z" }, { url = "https://files.pythonhosted.org/packages/2f/21/e3143f3f4714a971f964df89b26da10b1df861ca70b6a03a91170ec955d9/multidict-7.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:69eef00fab0af160d2b98dc59522a06005d9da8cc24c80f544e286ddcf29147e", size = 87344, upload-time = "2026-09-26T19:33:10.238Z" }, { url = "https://files.pythonhosted.org/packages/cd/73/15335f21f20679f2588c529156873f85050f13333cf65ec2fd1eade350e5/multidict-7.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c393c5a644b8114323c8e761e2eec64addfbad0dd631caadb69878c3feac443", size = 86418, upload-time = "2026-09-26T19:33:12.222Z" }, { url = "https://files.pythonhosted.org/packages/25/66/7947ffb65e28273006859d5f46348a2b7fb1f500d5c6965f29daa1a7b967/multidict-7.0.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d685c5206bb11d1f67fe1e9f463443c6cfdec8822373b437ea8cac6327c2ffe", size = 489579, upload-time = "2026-09-26T19:33:14.131Z" }, { url = "https://files.pythonhosted.org/packages/1c/0e/d1f0ad8bd4a972ac73fa4b99e17c9bf585bfe795fdb42c16eaaf5fd1f14d/multidict-7.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25ef0f87f75fb073bec751bac6f6608fe3d345e4e0ce8f4e8a67a670640c0e94", size = 497808, upload-time = "2026-09-26T19:33:15.861Z" }, { url = "https://files.pythonhosted.org/packages/30/bf/e26ef0a1d752b53aa1aa42e2564bcd810cc1cf15db98497651310ea85120/multidict-7.0.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c423928c97ac9c3250526278de9d8a62eeec7c47eb82bebb16508ffd6d5c2407", size = 449361, upload-time = "2026-09-26T19:33:17.71Z" }, { url = "https://files.pythonhosted.org/packages/49/a2/4e40d72b9510217b55cb456d09fafdc4c46c4a622aed7b69730e6d2999ca/multidict-7.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c32230fdb825a4f68f47d032ebdba85f9837278e691db6cb4049140953c8da3", size = 515274, upload-time = "2026-09-26T19:33:19.648Z" }, { url = "https://files.pythonhosted.org/packages/73/42/a81907c90ea1177aecb3b7886f5234f29442e9b863055428098a41009b80/multidict-7.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f89b0c320744651b4f8dfff1daf7530440c7ed4d5a5b3a2416b76184c2f258f2", size = 526354, upload-time = "2026-09-26T19:33:21.715Z" }, { url = "https://files.pythonhosted.org/packages/7d/8d/39dfaa9f27b70f9227ba262cdd65e3f573c3da0d54d54b130608019f2242/multidict-7.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eab61ed8ca26f4e5f846642c04af0ade0212923607536644730249f6adbb7d39", size = 505048, upload-time = "2026-09-26T19:33:23.599Z" }, { url = "https://files.pythonhosted.org/packages/e2/ce/fa6288b2591f5e8d73384259022b8de5b8abdbc51aea024b2019dba45932/multidict-7.0.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fab276e931979bf1f093dd12e7caf9838f333f319aa88c2654e4abb6532385d7", size = 447377, upload-time = "2026-09-26T19:33:25.426Z" }, { url = "https://files.pythonhosted.org/packages/10/6a/c586987468e641ce8ff0bdc6a0086619916decd80a529d1036ce83e3a668/multidict-7.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:acb774738bbafcb98ac7d0f332e85315c3441cf0238c739d60f750888a4eb16e", size = 486536, upload-time = "2026-09-26T19:33:27.343Z" }, { url = "https://files.pythonhosted.org/packages/27/67/71e9a2941c34eab9c9325747b8b02c203e657bda9003e3c797a36cbd4e31/multidict-7.0.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:be508246a4b5230efab9cd8b3e870f2bf8c5ab13f57d93e49833d14ab53cd4fd", size = 464531, upload-time = "2026-09-26T19:33:29.411Z" }, { url = "https://files.pythonhosted.org/packages/96/71/7f8f93abb590ed1431ee4c0b376d596684366f0ab773a809af5645126dc5/multidict-7.0.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f0cbe880f7a20eaae14203a4c592f2a239f6d834db96933cc4b7f8b2b9498372", size = 491494, upload-time = "2026-09-26T19:33:31.274Z" }, { url = "https://files.pythonhosted.org/packages/d1/c1/989082f922610bb69b570b9bb747ca14ec70b8e3b78240042b582298ca26/multidict-7.0.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:445d0237502a5bb0c937d1bf6f79993bf0679bb9639d80a6a0005490748bd559", size = 504691, upload-time = "2026-09-26T19:33:33.309Z" }, { url = "https://files.pythonhosted.org/packages/b2/3c/590a5a31cd42a854897411e962a12d44873c8ebf59363a2746ca24ce1183/multidict-7.0.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:15f882a4a16c19b7f3437a84bf1c2d10a327d9091c5b0d5953834e7e76e2058a", size = 444972, upload-time = "2026-09-26T19:33:35.408Z" }, { url = "https://files.pythonhosted.org/packages/b3/95/0767ef78015a364984d770051260b0ca305a7e5c458dcb1ab84b2ba5daff/multidict-7.0.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:39dafad0a1c7cfa92ff52319c4d9802e67404dad1f7261b155cc788408f2b680", size = 508639, upload-time = "2026-09-26T19:33:37.591Z" }, { url = "https://files.pythonhosted.org/packages/76/28/09012a7a7081518a52103db88fc3e9ea1c3d00a3ecff5c5b6423052bdb8e/multidict-7.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1910d04129509707aa90694c3ed1136ef3207f32bc0927f3b7e3d50630c42045", size = 500254, upload-time = "2026-09-26T19:33:39.546Z" }, { url = "https://files.pythonhosted.org/packages/44/9d/89e83250d8a60677c6cd4e4cf21b9a30b37801967c33bcb4eb8e82d7dd55/multidict-7.0.0-cp314-cp314-win32.whl", hash = "sha256:95f2df049ca3611f21ef433d21860d7523b869b8b45fcef815fa4a808955a7b7", size = 75941, upload-time = "2026-09-26T19:33:41.298Z" }, { url = "https://files.pythonhosted.org/packages/23/d2/83b5d1df0d2e7a2845535883e7a3228b120baf7bded4098838005fa49756/multidict-7.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:42f71f869830e99bcf55e8aa991b28a9869b9efc417e489d1e3f302a21a1edec", size = 86449, upload-time = "2026-09-26T19:33:43.291Z" }, { url = "https://files.pythonhosted.org/packages/11/35/8a2278ef66d0cae8beda52beca4519fac73221909ae9b0db74fea5d92aa9/multidict-7.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:a0e88d591c6a4a4ce618f813f2bf2854def1e7788434f658dbcb508cf6f853a0", size = 85179, upload-time = "2026-09-26T19:33:44.949Z" }, { url = "https://files.pythonhosted.org/packages/a2/d6/5708e2b0944081ee512d3db6b113db14e03c499a52930d819bce391ffd76/multidict-7.0.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6c813204ec59457d8df315e4408de32f095747ea26dbfa5385ede1baae7bc8fd", size = 161592, upload-time = "2026-09-26T19:33:46.853Z" }, { url = "https://files.pythonhosted.org/packages/3d/2c/b1c626cf6dbfb801779ed63a85540fce1998a33dbca151ce90853bf332ad/multidict-7.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:44c6d85b965d682731d2689d6667f39ba2122ac5ade6b49888701fce171a4a6c", size = 95087, upload-time = "2026-09-26T19:33:48.543Z" }, { url = "https://files.pythonhosted.org/packages/4b/2e/f79387e309cbe979047653bd8cd9e3f3561b45c360c5d7708fc78c97e104/multidict-7.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:753499a73ab1fae02592de81bed4d508cfe853c503870e2dea2f1c4efa8a09e1", size = 93905, upload-time = "2026-09-26T19:33:50.287Z" }, { url = "https://files.pythonhosted.org/packages/8b/da/2c9a51df02d0964c71306c514ff3dded4bea971019b4bbae518901999700/multidict-7.0.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4f47d515575944f88ddcdd1f5a71a9cd35da52a1596033e5a2851734d550a981", size = 447062, upload-time = "2026-09-26T19:33:52.074Z" }, { url = "https://files.pythonhosted.org/packages/17/a2/506e445dae9f39405d2ca1b317bb716ce04508ad6291c5b1bce651c7b260/multidict-7.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cecbfaa05d754f92568e79f2bfab65453c5512add27bdf0e414609d1756c2639", size = 469736, upload-time = "2026-09-26T19:33:54.238Z" }, { url = "https://files.pythonhosted.org/packages/c6/8b/113f36b4871f6caa65b0ee55f60b9eae500236e84d1397ec0832008707f7/multidict-7.0.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5184246bb78de1b06da02933cc0a1ceb8d93a781048fae4afa1541d6af72cfd5", size = 430142, upload-time = "2026-09-26T19:33:56.153Z" }, { url = "https://files.pythonhosted.org/packages/d1/29/e659d4038eb32d492c84f2c2ea8ff877560d3950409741163693b8133151/multidict-7.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b65c94459238c6cd8cdc3eb4ae0fc2aa27876ade2ae66b517c87e320b73f41e8", size = 475714, upload-time = "2026-09-26T19:33:58.243Z" }, { url = "https://files.pythonhosted.org/packages/81/b6/9e7cb7b5792597d531c132c10e21d50336d1426e56cd7a1fdb286e44578b/multidict-7.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2a7c1755dc12d03f15fd5031eb404c1535ff1ae41f4151c1d5eadf1e80c3957b", size = 480163, upload-time = "2026-09-26T19:34:01.597Z" }, { url = "https://files.pythonhosted.org/packages/d2/d4/d38dc48797be4eb586e43adcd625d4566ba98f8ad3711568ce200c17e14f/multidict-7.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0a17bdceb8447c94e55f5e6ffbe6ddfae03566d0b613b79ef5d5675342d41b0", size = 463815, upload-time = "2026-09-26T19:34:03.805Z" }, { url = "https://files.pythonhosted.org/packages/de/74/00f22c52e942d3c5d5f56461687713502bfb46832d650e6d19d4b80a02dd/multidict-7.0.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44894c3546f6dac974b1ca22fb0c1895bac6744e80fa52523067160e4f67ef64", size = 415806, upload-time = "2026-09-26T19:34:05.661Z" }, { url = "https://files.pythonhosted.org/packages/a3/c9/5fe7ff249c36e3b6ce3247120b052ee9073adece4ae2f9c3a8a525713045/multidict-7.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413e630e4f30db52b96ba8e4977a747e562c49c4896110eb50d67a4ba1a5b4b1", size = 455505, upload-time = "2026-09-26T19:34:07.69Z" }, { url = "https://files.pythonhosted.org/packages/58/31/89207ff677a31804728772d394b49025bc65ad0dd502865fb1b43bc28226/multidict-7.0.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:39a930227bcba060ca7583211ca2eb0f1e5a5e8a5c53e5e5f05c5dd867cc05df", size = 435297, upload-time = "2026-09-26T19:34:09.644Z" }, { url = "https://files.pythonhosted.org/packages/ca/b7/086d7ff9359160fb09e01b8e81abd72b4dba86ae9124458cdc74d377a059/multidict-7.0.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:71fa4c31f5dd02feeb3103f4aafd81129df071bec5909edb7bb69670aaace717", size = 444247, upload-time = "2026-09-26T19:34:11.692Z" }, { url = "https://files.pythonhosted.org/packages/ff/b3/d935f02b158872ba31277dc66bc9939c7f6b4b10ed17056ee0f923d93e54/multidict-7.0.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b3830cf41a6ee76c50454fc504c4ecd046dcc9c201647bc252ac8df2f430cb35", size = 461613, upload-time = "2026-09-26T19:34:13.933Z" }, { url = "https://files.pythonhosted.org/packages/82/e8/6031259fec901717ce47a022f392e90597ee2e2995679bfc11fba71229d2/multidict-7.0.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3089f229608c5548aad1eea08d43cfa35a648f34fb508dacfe86df8be2468ee0", size = 414713, upload-time = "2026-09-26T19:34:15.951Z" }, { url = "https://files.pythonhosted.org/packages/45/bb/db8a1c3d18498e0a5339682077f3e9f5a05de19d3e49d84825cd56f56624/multidict-7.0.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fd418115d9eb5731c281a18141a87a84ae5210aa59ffcf4ecf69eaf8428ed88f", size = 459246, upload-time = "2026-09-26T19:34:18.799Z" }, { url = "https://files.pythonhosted.org/packages/23/21/2db39e324511b4f1eafa9a1d6fad9f7cf6385dd9b5489bac3e5fab9c0c3f/multidict-7.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:377d472e510f6270db2644d06172b25ab43ad91087b9eef25e7b0c38408d9abd", size = 457832, upload-time = "2026-09-26T19:34:20.805Z" }, { url = "https://files.pythonhosted.org/packages/f4/d9/e155b7f9e343a48a61888daa104298dc37b5b94414202a58ffa9b4f47a69/multidict-7.0.0-cp314-cp314t-win32.whl", hash = "sha256:c3f1f667f16da3410ac2ede289752ddb1e3a40e468520210dd34da4627446d22", size = 82971, upload-time = "2026-09-26T19:34:22.991Z" }, { url = "https://files.pythonhosted.org/packages/98/0e/d8ed433f066919ac26cbc0ee4e87b9653c8f8dc6e01d6d353d73c49a5ebc/multidict-7.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d9947b2717bff03c63db62dccc5ab733639d02cce77dc87a65c288e178c8204b", size = 93534, upload-time = "2026-09-26T19:34:25.114Z" }, { url = "https://files.pythonhosted.org/packages/da/39/0fb7d550deba553d493cfaf9abd01752ec319a8e09e5bc4cb6fb7d708920/multidict-7.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:41cde24068c3c362bc6ae60d483d86604e9e2792a6b530afeb825d1d2ffbd07d", size = 90632, upload-time = "2026-09-26T19:34:27.019Z" }, { url = "https://files.pythonhosted.org/packages/cf/1f/9bc2fc833a18b518be8133dfa5941ae62cfed2783a7eb9b81e0543e2c011/multidict-7.0.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:dbd55a8c0c1296b7ca384371745f83e8c87a4194312761aaa3a6790551dc2840", size = 91531, upload-time = "2026-09-26T19:34:29.337Z" }, { url = "https://files.pythonhosted.org/packages/1e/c0/8a1e72e9a6432a6ba22079aec5364625de2758d1046c7f137dec50d59445/multidict-7.0.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f2703bb73c2baa2dd5aac102ccf08daf39dc7cac40ba374a6ec77731644d037c", size = 84648, upload-time = "2026-09-26T19:34:31.246Z" }, { url = "https://files.pythonhosted.org/packages/f8/cd/8128811f28cf53b909dc74fe2b75d19da89e121c63c2de8987ca9d9012e2/multidict-7.0.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:96a4db7bf197f80806aa6104ded739ca0d9dd7a16cc2ec727c84d7651a7e0759", size = 86456, upload-time = "2026-09-26T19:34:33.209Z" }, { url = "https://files.pythonhosted.org/packages/7a/7f/a2b6e23539d6c9da4ad31775f1260b4043566b1aafbc42b6ad25151fd686/multidict-7.0.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:5e1e0091a0cc8dc15a879eb514eeed6de15fc362117fb2e2cdf9c546e93480b5", size = 146137, upload-time = "2026-09-26T19:34:35.114Z" }, { url = "https://files.pythonhosted.org/packages/29/66/c44f76233d92d8cd29b6fd8f5c39fca07a35c4d872ed65addbe4012af062/multidict-7.0.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bcaab97a69e30d3adc88441a1313e9070fed0025f12f9c7faf6b3c9d8581dec6", size = 87349, upload-time = "2026-09-26T19:34:36.995Z" }, { url = "https://files.pythonhosted.org/packages/5d/d5/64f37fd53602b6692a6b67cc52eabe8e2e1c47b119269c1f19e77d16b042/multidict-7.0.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0707a1147bb2ea331097400cf5aa54c3b76971c9d96680d299496e43bc4bcf71", size = 86560, upload-time = "2026-09-26T19:34:39.047Z" }, { url = "https://files.pythonhosted.org/packages/d6/9f/f3c64fa718234894420da614b8a80253f5693394b8597905e99dd5ae9d44/multidict-7.0.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:afb4cfa516496af5673daa0dc437b64db129dff3a918b1fdb3d8919c29b716a6", size = 490868, upload-time = "2026-09-26T19:34:40.893Z" }, { url = "https://files.pythonhosted.org/packages/c8/50/54da12021cbf8b1cf6aaedac0ac9e6a2f5e819139bfb64e0d8f77f2fe3b0/multidict-7.0.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38f50041fe20066b3a0ec148d7ecab32653a7937f30a7afc91076bcf3bd34c77", size = 498640, upload-time = "2026-09-26T19:34:43.046Z" }, { url = "https://files.pythonhosted.org/packages/dd/a3/755fe958af413bc113e5a8b9a68cde740c7e1f00a998b938c18abd384ef6/multidict-7.0.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e14442906c55056540923c9bf205918ac9583c8d0ebbfe249e25ab09d45cc594", size = 453905, upload-time = "2026-09-26T19:34:45.311Z" }, { url = "https://files.pythonhosted.org/packages/06/62/0c1ad55ece8f944f1c573210e731e80568c5bcb4133252c885360cee47d6/multidict-7.0.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebe89e2eb988fefafcf947e29e5a42ba60e92e1dc19c5c8e6a8e5c320519a9f0", size = 515736, upload-time = "2026-09-26T19:34:47.469Z" }, { url = "https://files.pythonhosted.org/packages/26/e8/f75258b835b26f53b5e7a5fb4d1f19876f55dda5927f370f10a89a8cedd1/multidict-7.0.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e5c8b2b5229b45ffb6b05ea02e2ff106a3b4072efea13bbd5883bf6dd627a8b", size = 526659, upload-time = "2026-09-26T19:34:49.885Z" }, { url = "https://files.pythonhosted.org/packages/c5/bd/ba9f6a9bacd303a1884c85e5b8053ba8ce75073501f94f4a99a3a4b058ea/multidict-7.0.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2296f1410e2d8ac4ad6458e0e768b2df456601ec6962d9e65f055e85519dc13", size = 506050, upload-time = "2026-09-26T19:34:52.061Z" }, { url = "https://files.pythonhosted.org/packages/8f/e9/38710cb6f1fdac722d2a43e7fa9830914545be5155c4247262da214c30d7/multidict-7.0.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27d0b6d6fe01cb243bd21887bdf0b5ab03e098a6a3997d1b0f70200daf72a427", size = 452206, upload-time = "2026-09-26T19:34:54.235Z" }, { url = "https://files.pythonhosted.org/packages/9f/5f/b560c7e61789523a683446374870d36f9b4ceee0e853f5269a3fc878a62c/multidict-7.0.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:52d8e990a3589ddedde3dc106d01b68edd117e585604e473aa7d76d633689fff", size = 487115, upload-time = "2026-09-26T19:34:56.391Z" }, { url = "https://files.pythonhosted.org/packages/9c/b3/6f38ae930f0a0ff6dd0ca037cdd2428affb8eb549302e079effbd9956286/multidict-7.0.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6bfc8da8e44c19140e4f010b73f9405bf054606ce4d30e757770ef7a0617db1e", size = 465888, upload-time = "2026-09-26T19:34:58.6Z" }, { url = "https://files.pythonhosted.org/packages/43/7c/073a8e070c3469bc2319caf4197382eb1bf5b151fb5ead4ca4a6e6a1554b/multidict-7.0.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:4203b31c67b5b4aa22c8af0a36ccd037364d07761495afb35498c3cd8487d914", size = 492765, upload-time = "2026-09-26T19:35:00.768Z" }, { url = "https://files.pythonhosted.org/packages/6c/31/5ff6af405404e0542e92602df4d81095a08c91bbf872474fb49ce1b192ec/multidict-7.0.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:ef7752fc962fa01363b594ad6dc1477e30973dfc78f374e3cb7d40c248a22def", size = 504980, upload-time = "2026-09-26T19:35:03.152Z" }, { url = "https://files.pythonhosted.org/packages/80/b2/3c19af5797b9764db81baaa97ecc70aef938bcf9b3907f12b3470cd29e00/multidict-7.0.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ac57697b41e05abc2ae2c9a133d2d75eaefcbcdacf1b4c043997dc3bacbc950d", size = 450431, upload-time = "2026-09-26T19:35:05.479Z" }, { url = "https://files.pythonhosted.org/packages/3d/71/104a44ea317ac070954e2ccd28fbacd795643b181c1957d972fa03cb88c5/multidict-7.0.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:6999e733527c8095cfc6f7488ef525e27f0d67795b6d64a503a7d52ea92d5020", size = 509830, upload-time = "2026-09-26T19:35:07.833Z" }, { url = "https://files.pythonhosted.org/packages/af/8d/f936137280da0562e3b7d36bd994ce6e114f29a57efa89dec4d5044d3a98/multidict-7.0.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ce8ac834849bbf034ff74f631d8e60123b756a85b0172630cdc1c6eff747189b", size = 501504, upload-time = "2026-09-26T19:35:10.235Z" }, { url = "https://files.pythonhosted.org/packages/46/af/c9ad628752a09fd6f77ff097587bdf5e9a29d3b0f3b489d6708f8017a16e/multidict-7.0.0-cp315-cp315-win32.whl", hash = "sha256:675fe5ccf9bdd6e6e17540609860a24e0364678cf37f71717fc55c9dd788d718", size = 75939, upload-time = "2026-09-26T19:35:12.66Z" }, { url = "https://files.pythonhosted.org/packages/65/d2/8ec4e51497672ebcf3731c4f12333eff1e1b24eab7ef511da2ea996e9694/multidict-7.0.0-cp315-cp315-win_amd64.whl", hash = "sha256:b22d8c7a8943b57be846685f78c04c3fe302857613b00d42e3cc1279f0968b44", size = 86464, upload-time = "2026-09-26T19:35:15.004Z" }, { url = "https://files.pythonhosted.org/packages/d8/1a/6c76a93b16d8cfec84195d21ed79791507e2f55ba0cbc23ab10e68b5308c/multidict-7.0.0-cp315-cp315-win_arm64.whl", hash = "sha256:ab742ffd1146a81b985fb2bc96c8d31e4c4ba4c99739ea4e55e8d40769369ae9", size = 85190, upload-time = "2026-09-26T19:35:17.138Z" }, { url = "https://files.pythonhosted.org/packages/5a/95/4d89de7ac06fbdd353957986aab3c187479dc33ad9bfa1d3521f947b02dc/multidict-7.0.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:202069bcc49d33ca18c69967f8edc012de2dbbfa064ae2840031ee39e598924f", size = 161581, upload-time = "2026-09-26T19:35:19.241Z" }, { url = "https://files.pythonhosted.org/packages/52/b3/478b1326d85fb28fc299c1e4adfe6a9c0c6cd0d8440944de9d4cd3cd5bc9/multidict-7.0.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:90e3e7d79710ee706c76350f0d4a38fb000ed33880dc244274e93c0daa4e2e0e", size = 94972, upload-time = "2026-09-26T19:35:21.461Z" }, { url = "https://files.pythonhosted.org/packages/68/6e/70f658a4de98d87dfcd160013f8923a8ff0a6bf921f5b55ca84c10a52723/multidict-7.0.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ade9ae493a8152d0181293860a73a2273914eb896f19d0b220ba7d1095947c94", size = 93995, upload-time = "2026-09-26T19:35:23.7Z" }, { url = "https://files.pythonhosted.org/packages/f1/38/9a25ac04c73e8ef95c378c27ac66df00edbe3f688b0c03555815570bc5b5/multidict-7.0.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:586ac40ae0a2ed5bf7e4a162889276775557f4008141f43f3837f8a1fdc722ed", size = 450149, upload-time = "2026-09-26T19:35:26.137Z" }, { url = "https://files.pythonhosted.org/packages/cc/20/f7969f851a29106120cc7972d7d618089d65736f42bc5a59cc9a06eea091/multidict-7.0.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25563e771bc70562805cd1d9f614857a9d8a2e1eb39a5ac0df00c5fb28f5882c", size = 468031, upload-time = "2026-09-26T19:35:28.671Z" }, { url = "https://files.pythonhosted.org/packages/0e/b8/aba2e168fdae9bba256fcc39d42208e49c4d80b5c2953d3dfda7651778aa/multidict-7.0.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:598d79465e2ae5fe83e150d4dd28435073c5d0a7546d72be707590a5aa8c6efc", size = 434495, upload-time = "2026-09-26T19:35:31.133Z" }, { url = "https://files.pythonhosted.org/packages/56/74/fdf98439bb25f40c604b4ddd297672523b59ecb7350381d34cf6d1f0f5f9/multidict-7.0.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4f339460a931f6df4760ec969b6df20f10722cb23c1563abdb91e8c44c72ce6b", size = 475948, upload-time = "2026-09-26T19:35:33.492Z" }, { url = "https://files.pythonhosted.org/packages/63/9e/3497006c588ddf20bf35a7d5c3a5c5a1cfb4d30e601eb37b38b6f1ad4682/multidict-7.0.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:160c08680aa68bae65c95bfe9a07876746b53712527b67882644eb20848b7143", size = 481562, upload-time = "2026-09-26T19:35:35.912Z" }, { url = "https://files.pythonhosted.org/packages/ad/a0/4ca6ac0b157261d38ac424a1902a69efd879de8f0b2135d5416c2d6453da/multidict-7.0.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34ad65977ba724d67f85635c06852b8e8e3de649f2502b0cc7c9ef90e1352cdb", size = 467085, upload-time = "2026-09-26T19:35:38.333Z" }, { url = "https://files.pythonhosted.org/packages/0c/87/637eab6cddb15ebf8438e25cc19982630e507ee30944c24c8de34f09f65c/multidict-7.0.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1577624c44b455da7c15c8ba923fa05afcadbdeb46c8f27ad5f26ba50577e12c", size = 419870, upload-time = "2026-09-26T19:35:40.622Z" }, { url = "https://files.pythonhosted.org/packages/65/97/f7a264baa66e1ccfc42a564db3fcb4038e93daac70e003dee4f5dcda0692/multidict-7.0.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:1f2967aece3b09780e02cca98f8b9f2b6fa03d44fad50f7f73ca11e5c419d75a", size = 454369, upload-time = "2026-09-26T19:35:43.139Z" }, { url = "https://files.pythonhosted.org/packages/25/44/1921705ac53d623e5dcc350c4effa321e6e9cc641b1b1c4a70b97edba13a/multidict-7.0.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:89b34996de4988b78f55fffb1af882f69cea3ddec1bd5c98a005363bcf4a462c", size = 440313, upload-time = "2026-09-26T19:35:45.581Z" }, { url = "https://files.pythonhosted.org/packages/16/fb/45c96a610c4ec061d3da1a485abb436eaa0caf2a921ea7bd56c05fa8a3ac/multidict-7.0.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:c0286fc15d4c3794bb5b22da0688ae9e4227718f0682b45098b043e901d878fe", size = 449727, upload-time = "2026-09-26T19:35:48.108Z" }, { url = "https://files.pythonhosted.org/packages/ad/24/6f3666bf9203c49c798315a76d600cbb23690196977810b80c5f2b90c467/multidict-7.0.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:45cc9beb3b101a355708b0124b8271846289e7631f8127c43e6197ea32d5716c", size = 461935, upload-time = "2026-09-26T19:35:50.501Z" }, { url = "https://files.pythonhosted.org/packages/da/1e/1b648baa8070ca362e39df5b29114e02c0fb6dd02b5cc721311d3f9695c9/multidict-7.0.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:8242568856ba8f81b1f55a9a52e54acf72dd45e52f80b4779c55d270a4cd5136", size = 419127, upload-time = "2026-09-26T19:35:53.977Z" }, { url = "https://files.pythonhosted.org/packages/da/98/aeb8c8c989c59dfaece10ea6d32bf3b77ae5100a89a13ea3b512a004b447/multidict-7.0.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:2c3ccea33d092b5b4f951df3d8426550dae23888b70d0675b78a899aab3dea97", size = 463352, upload-time = "2026-09-26T19:35:56.617Z" }, { url = "https://files.pythonhosted.org/packages/de/67/b43af75a351197e9e3babcede7fb37d9116d685c6ad911f11a83dc75e80e/multidict-7.0.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1425b857bf5e75f3d34e502919d884aef6e2a1df37e0a5a44f1da8c7a973ad8f", size = 460544, upload-time = "2026-09-26T19:35:59.454Z" }, { url = "https://files.pythonhosted.org/packages/d6/be/dbd67bd5926ea07228835eb41c6f34c0a3c2aae36e6ae1141e32ba350326/multidict-7.0.0-cp315-cp315t-win32.whl", hash = "sha256:5277241aa2a858fff9f0a06366ed3ce3c25f5bd021013979627b49ccadf21f32", size = 83146, upload-time = "2026-09-26T19:36:02.074Z" }, { url = "https://files.pythonhosted.org/packages/1c/1d/8c3ec61f539c52f4def6aa3f0d6565d573c89e141afdb97af38ce0f0647f/multidict-7.0.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dab80f71b3b7875be6e739ba2a840aa0dd0e25feca479e86b9f27238de4cf7d5", size = 93683, upload-time = "2026-09-26T19:36:04.659Z" }, { url = "https://files.pythonhosted.org/packages/e5/03/c42d9e70109bfbf93093f7bbe98c6ea1d559f790ad86b572fda133ac9224/multidict-7.0.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cf1c05235c53e2d93684d68e9bcf6e64f5fba8d74d22cb1bd92b94af62f588ac", size = 90896, upload-time = "2026-09-26T19:36:07.407Z" }, { url = "https://files.pythonhosted.org/packages/46/a4/a3e9e72ed0f99587e9e96c3de72b743e1385e5e2b15fb583d8dfc05ffffb/multidict-7.0.0-py3-none-any.whl", hash = "sha256:1ba6a45450243bd8953223adff7183fce5e3f9f3556a03bd17a674caaa046215", size = 26592, upload-time = "2026-09-26T19:36:09.764Z" }, ] [[package]] name = "packaging" version = "26.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] name = "pamqp" version = "5.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/10/3a0d1040a1bac414ef1b4cb9b6f53344b6bc33e57eef287ac58d6dc1d1de/pamqp-5.0.0.tar.gz", hash = "sha256:d76dd4af900802483cda5f318a5328c86a1e9b127159f5455b17d9a0bb658b89", size = 137474, upload-time = "2026-10-30T15:41:57.894Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/ac/fd44d9085dc1270e9f6a9eae0a08bbc69d8354c66623c647fe45ecd4b97d/pamqp-5.0.0-py3-none-any.whl", hash = "sha256:f426351b57f6a9e453ee27682431e532c44771cb61b4f74018b8a7088f4cac20", size = 32793, upload-time = "2026-10-30T15:41:56.381Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "propcache" version = "0.5.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/40/14b21e505b7921617466576423f188a5c9caddfdaa1cf4b2b8a83d8fe216/propcache-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e", size = 86393, upload-time = "2026-09-16T00:14:06.9Z" }, { url = "https://files.pythonhosted.org/packages/e7/4b/5a52e1a7b43563f7d408814194bb23cc8bf214eb6b86639b667a33a8d0d0/propcache-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d", size = 50431, upload-time = "2026-09-16T00:14:08.025Z" }, { url = "https://files.pythonhosted.org/packages/05/cf/b5248180bf056cc76acc60c9c6e8c0ebbfdbd1c6cffd31fd14996927b7c8/propcache-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d", size = 52116, upload-time = "2026-09-16T00:14:09.114Z" }, { url = "https://files.pythonhosted.org/packages/86/a8/7c6cd6bfead1a11f2e411e688640e6d26574cb0bde7dcaa7423b0b65ed7a/propcache-0.5.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8", size = 238729, upload-time = "2026-09-16T00:14:10.357Z" }, { url = "https://files.pythonhosted.org/packages/5c/b4/442715b2e980df51be52d203549279e027728f24c80b00b5e525e31cd5ea/propcache-0.5.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838", size = 246121, upload-time = "2026-09-16T00:14:11.735Z" }, { url = "https://files.pythonhosted.org/packages/bc/5d/df0684fc2b1732a01a7bec26d7897369022712422d25b09c37ce7dbc88a2/propcache-0.5.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757", size = 251735, upload-time = "2026-09-16T00:14:13.053Z" }, { url = "https://files.pythonhosted.org/packages/c7/06/519a5ebb48b6f94beb48396e55c905f12246a25c3a3608a7ec7bceabf50e/propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5", size = 235381, upload-time = "2026-09-16T00:14:14.398Z" }, { url = "https://files.pythonhosted.org/packages/3c/07/1e0a9bb310830f2245edbd5cd3c6d24a783c053c4efd8e08e386e513c940/propcache-0.5.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5", size = 208973, upload-time = "2026-09-16T00:14:15.715Z" }, { url = "https://files.pythonhosted.org/packages/62/5c/9324fab27d6088eecc47fe4332bf7aaf8c1ded93c36f558391e8a06d41a7/propcache-0.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8", size = 233897, upload-time = "2026-09-16T00:14:17.25Z" }, { url = "https://files.pythonhosted.org/packages/9c/a3/570d92fc952eae93b676f3a1568f4b89264102abd3c982ab6a9ebec58dcf/propcache-0.5.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b", size = 223512, upload-time = "2026-09-16T00:14:18.87Z" }, { url = "https://files.pythonhosted.org/packages/65/47/26810d889d89bba31db397e6a88f8984af775f5ed6bad0a29dce84324cff/propcache-0.5.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6", size = 239043, upload-time = "2026-09-16T00:14:20.366Z" }, { url = "https://files.pythonhosted.org/packages/89/2d/f9c47691aa024c8299a3afacd78d22a01ab57eb627b481b6089708e71017/propcache-0.5.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36", size = 208218, upload-time = "2026-09-16T00:14:21.801Z" }, { url = "https://files.pythonhosted.org/packages/74/6b/d510c0c378cabbf9d0ac7b663af6d00f2e9074073d93b20c85f24aa5c071/propcache-0.5.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d", size = 240301, upload-time = "2026-09-16T00:14:23.121Z" }, { url = "https://files.pythonhosted.org/packages/3d/80/c80f6adaaa1e51f0db2dce8c9b3714d94ec45e358a21f9a1910b10b40a80/propcache-0.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd", size = 230785, upload-time = "2026-09-16T00:14:24.458Z" }, { url = "https://files.pythonhosted.org/packages/d9/e2/c32a7df3f39caa7f11b2eb37ea5b6960a6946f2bc7c4b8ff97bbdf6d6b6e/propcache-0.5.4-cp311-cp311-win32.whl", hash = "sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9", size = 42747, upload-time = "2026-09-16T00:14:25.715Z" }, { url = "https://files.pythonhosted.org/packages/0a/8a/3db6a3543d8101263b4c52978b6276a04ead2caff2c5ab880d934f47bd89/propcache-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858", size = 46268, upload-time = "2026-09-16T00:14:26.996Z" }, { url = "https://files.pythonhosted.org/packages/41/07/5222e2665bbf6e45847492ecbf3b9f3e4975a0ae300e5fd465df7d48ce55/propcache-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39", size = 43547, upload-time = "2026-09-16T00:14:28.143Z" }, { url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" }, { url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" }, { url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" }, { url = "https://files.pythonhosted.org/packages/83/4f/48865bd02a16ee5236bc46166b2946f37b93e07b0eae355dac0be0b216ca/propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef", size = 251768, upload-time = "2026-09-16T00:14:32.908Z" }, { url = "https://files.pythonhosted.org/packages/b0/19/3742a5eed62317b03b4002ee865dc9fd720308bdd0da1f29a5786c630311/propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f", size = 257723, upload-time = "2026-09-16T00:14:34.267Z" }, { url = "https://files.pythonhosted.org/packages/cb/d5/ee6350fb0be9122bb6c67082a876d34b90d980d100c106af4b81023e04f4/propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546", size = 265597, upload-time = "2026-09-16T00:14:35.56Z" }, { url = "https://files.pythonhosted.org/packages/85/9f/83a07b6ec0e043c050cfdd35fb0cf1b7897b91d554d6eea293740309afe7/propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b", size = 250424, upload-time = "2026-09-16T00:14:36.894Z" }, { url = "https://files.pythonhosted.org/packages/33/2c/a763a8251f50fba042af0fb1f02bfec4b31381e40aff760db2be7b2e1f84/propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab", size = 216748, upload-time = "2026-09-16T00:14:38.369Z" }, { url = "https://files.pythonhosted.org/packages/6a/e2/4d11bea8fd6a777149c6c20645f873952eab5de3a2497aa11648ec9ab6ab/propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc", size = 246533, upload-time = "2026-09-16T00:14:39.82Z" }, { url = "https://files.pythonhosted.org/packages/9f/36/6683597de4907e70c717e3588c541202c66086a72ff3db58be49de66e72c/propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937", size = 238173, upload-time = "2026-09-16T00:14:41.259Z" }, { url = "https://files.pythonhosted.org/packages/85/84/cb08d79f1762daafeb2b030c470cd0c725c97b8ad67412457c6f35c53e9d/propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526", size = 251128, upload-time = "2026-09-16T00:14:42.652Z" }, { url = "https://files.pythonhosted.org/packages/c2/0d/41b848036db6621370c1f2e5471a7da8149c730f8552a5257567721f4576/propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944", size = 214821, upload-time = "2026-09-16T00:14:44.112Z" }, { url = "https://files.pythonhosted.org/packages/f1/b7/adfae4bf9c63bccf12e2d9690a175c6579047a6eec3b5a6a5f51428c15e2/propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031", size = 254793, upload-time = "2026-09-16T00:14:45.429Z" }, { url = "https://files.pythonhosted.org/packages/51/6f/eeca9647245d5f92e87d53e5f14335bb42fce1a7e6842c8045b364eded8b/propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161", size = 247134, upload-time = "2026-09-16T00:14:46.976Z" }, { url = "https://files.pythonhosted.org/packages/5d/a9/424e38838793d37160b4379c702f61c74c598fc6cd17204adbe3c554f7a8/propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8", size = 43073, upload-time = "2026-09-16T00:14:48.338Z" }, { url = "https://files.pythonhosted.org/packages/58/7b/6e8ef26f6d510a7916064fec68d55fcbfbdf7eb01e377480d66a122152d8/propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb", size = 46190, upload-time = "2026-09-16T00:14:49.99Z" }, { url = "https://files.pythonhosted.org/packages/08/b9/72028c5b56ced97f456de6aefa79435ca64d7f77af78ea8cf3c76fc5195f/propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737", size = 43075, upload-time = "2026-09-16T00:14:51.155Z" }, { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" }, { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" }, { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" }, { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" }, { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" }, { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" }, { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" }, { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" }, { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" }, { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" }, { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" }, { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" }, { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" }, { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" }, { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" }, { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" }, { url = "https://files.pythonhosted.org/packages/33/c9/07e227b930c8ae513b8ef1aae3793499be097bffcdf7aee4fb8b33db4cd1/propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565", size = 85933, upload-time = "2026-09-16T00:15:16.073Z" }, { url = "https://files.pythonhosted.org/packages/e4/e1/6710bb44510c4e4a8e0f004bbaf3cecfd048141309c77bae56d4e5a6ebc1/propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993", size = 50179, upload-time = "2026-09-16T00:15:17.377Z" }, { url = "https://files.pythonhosted.org/packages/e2/22/b533b493d7025456f44518b33e53e000021a20fe7c27b88cf3d341df7186/propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b", size = 51942, upload-time = "2026-09-16T00:15:18.589Z" }, { url = "https://files.pythonhosted.org/packages/f1/74/70ac8430e28f21e442c7bcb964eb46c4363f6881ade4aa0e978bfd8d503a/propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc", size = 232647, upload-time = "2026-09-16T00:15:19.905Z" }, { url = "https://files.pythonhosted.org/packages/72/95/f222f13b6fe623310be0eb61a673bf26df439ce27e563ca8e422d0818777/propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d", size = 241541, upload-time = "2026-09-16T00:15:21.3Z" }, { url = "https://files.pythonhosted.org/packages/a2/3e/763e370340db16115c5e63ad46e21ef0770a7f06928b3d3b62d8f8edfca4/propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f", size = 245332, upload-time = "2026-09-16T00:15:22.802Z" }, { url = "https://files.pythonhosted.org/packages/96/d3/e97cd6f5de2176bd90ed4076c7a9b5e09d0f0b9687d00a576507988bb62c/propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9", size = 232757, upload-time = "2026-09-16T00:15:24.374Z" }, { url = "https://files.pythonhosted.org/packages/f9/4c/6766e5f60bcda26d244333aa71d0a702c1c9b21b251d543c7af5953d1eee/propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf", size = 204389, upload-time = "2026-09-16T00:15:25.667Z" }, { url = "https://files.pythonhosted.org/packages/b8/5e/ec4bb09a70b26ea99d76a8292c3383b960b296de2b347ac9986678f1761c/propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d", size = 228217, upload-time = "2026-09-16T00:15:27.11Z" }, { url = "https://files.pythonhosted.org/packages/e1/7d/b53922ba7d9e5bf797324e63aa05906ec240871899f779628df068743e2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3", size = 216947, upload-time = "2026-09-16T00:15:28.532Z" }, { url = "https://files.pythonhosted.org/packages/ff/39/b62eee45e5ea4de094a258cbb3b01c1e856ca51ddfd95b43135c5effd1eb/propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc", size = 233457, upload-time = "2026-09-16T00:15:29.977Z" }, { url = "https://files.pythonhosted.org/packages/cc/a9/feec61ed296d993db9dd097e0f6723e3f576a647722367547495e4c5b05c/propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d", size = 204131, upload-time = "2026-09-16T00:15:31.74Z" }, { url = "https://files.pythonhosted.org/packages/92/4d/411ef380cddad28dc001f1c6d75ec72c76cd3817030f68ec1ccfba0ec6c1/propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792", size = 234820, upload-time = "2026-09-16T00:15:33.087Z" }, { url = "https://files.pythonhosted.org/packages/15/37/c988229753629ef1cfd5198337a83e624780ea2b3787efe9e747c05aad2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b", size = 228350, upload-time = "2026-09-16T00:15:34.533Z" }, { url = "https://files.pythonhosted.org/packages/12/49/5ef1c5cf98591da3c5b952b39e6a298084cc1ce353bc70f85e82397a5036/propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea", size = 43578, upload-time = "2026-09-16T00:15:35.957Z" }, { url = "https://files.pythonhosted.org/packages/1e/9e/a0ac821a2229186af5e2e3c3635a78abb23cfddca57f38513ab5d70420f3/propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983", size = 46304, upload-time = "2026-09-16T00:15:37.655Z" }, { url = "https://files.pythonhosted.org/packages/a1/19/c8d0d36a9d16cba5dcee67d389c9333b988c8986a653a61c00a451817a46/propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac", size = 43440, upload-time = "2026-09-16T00:15:39.091Z" }, { url = "https://files.pythonhosted.org/packages/2c/e9/42f1da77cacfc184e6ec929557ef653b7961bbf6f1da460b9221273948b3/propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0", size = 90672, upload-time = "2026-09-16T00:15:40.306Z" }, { url = "https://files.pythonhosted.org/packages/cf/2f/4b79940908c6ab8c795097c102999d7bc1f7e0b8604dfd1c232f9d99d67a/propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5", size = 52586, upload-time = "2026-09-16T00:15:41.575Z" }, { url = "https://files.pythonhosted.org/packages/eb/07/02196ae6320c110235bb343f90dbd34be41f8b8964a3ee30db84ec12579e/propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d", size = 54335, upload-time = "2026-09-16T00:15:43.027Z" }, { url = "https://files.pythonhosted.org/packages/6f/44/f48b9a131985659924df5fa5093f68fe72c7ee375329802989ba3126efc6/propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788", size = 297567, upload-time = "2026-09-16T00:15:44.373Z" }, { url = "https://files.pythonhosted.org/packages/04/a1/418d956d2735139f77fc35262179f1f52c23aa666de5a8ab3819c1ae7854/propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56", size = 297477, upload-time = "2026-09-16T00:15:46.048Z" }, { url = "https://files.pythonhosted.org/packages/69/fd/ff811fdb6d3d3e67fd9bbfb75881675d34a42d0ef29a45d33e3e233dde07/propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574", size = 302669, upload-time = "2026-09-16T00:15:47.458Z" }, { url = "https://files.pythonhosted.org/packages/fc/57/527910c455b5ec62f6871bef45d4f79fea16cb8c966ba0d4a07f0339ddc4/propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370", size = 287908, upload-time = "2026-09-16T00:15:48.99Z" }, { url = "https://files.pythonhosted.org/packages/1d/86/f69ab82707534a0cb2057bdca04f9200a71214c7551800f9d34d6ac39e4f/propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320", size = 249804, upload-time = "2026-09-16T00:15:50.486Z" }, { url = "https://files.pythonhosted.org/packages/27/19/60677af50d93be4256213de7cd487f056944c048b9c0b6f2e45b3a30f666/propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9", size = 282344, upload-time = "2026-09-16T00:15:52.029Z" }, { url = "https://files.pythonhosted.org/packages/7c/f7/a0057808a91fb3b6a5f3602b528f0cdcb3d53e0ff8315d73fabdfdf8fec4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7", size = 270167, upload-time = "2026-09-16T00:15:53.466Z" }, { url = "https://files.pythonhosted.org/packages/83/c8/f4a865490df0dc0c8531d4e59ac411cb6dc24bb255d2396a6f1c60a368f4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4", size = 286551, upload-time = "2026-09-16T00:15:54.995Z" }, { url = "https://files.pythonhosted.org/packages/b0/67/b4faebde9da4e8173d0e5a30e8cd31335914af7ef350b988f27fec588cfd/propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9", size = 249595, upload-time = "2026-09-16T00:15:56.505Z" }, { url = "https://files.pythonhosted.org/packages/f6/40/52e1dd5636e9f5a27f6b5a4b4e2f33c322fd72afe956c397d82523ec4a80/propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e", size = 286700, upload-time = "2026-09-16T00:15:57.985Z" }, { url = "https://files.pythonhosted.org/packages/d5/0e/30b2b324b93ff31a0bab539c102aae59e84e444031b2742150a7646aa1bb/propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57", size = 280500, upload-time = "2026-09-16T00:15:59.487Z" }, { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" }, { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" }, { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" }, { url = "https://files.pythonhosted.org/packages/58/25/895a11d1e4c5c2acc6d816e2bece34e02d9dc92f2182ae276cd819e9e804/propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760", size = 85634, upload-time = "2026-09-16T00:16:05.599Z" }, { url = "https://files.pythonhosted.org/packages/58/41/c0acd69271de7a1cf439e77d5d60c18575fd09bad56e798b95fa23458ea4/propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51", size = 50084, upload-time = "2026-09-16T00:16:07.384Z" }, { url = "https://files.pythonhosted.org/packages/a5/1a/ad561f99f90884089e6403b76c220610809429ba868a81a2e7ce115d32e0/propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae", size = 51692, upload-time = "2026-09-16T00:16:08.956Z" }, { url = "https://files.pythonhosted.org/packages/e9/07/057bdd3a9609ffad59b06239cceee784b047f6c720247bfaa36d2103e138/propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51", size = 232947, upload-time = "2026-09-16T00:16:10.466Z" }, { url = "https://files.pythonhosted.org/packages/fa/dd/d36ad35986718530498a65e45e3713f9f0e6a580f192ef02d2ef7cae9b52/propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1", size = 241250, upload-time = "2026-09-16T00:16:12.056Z" }, { url = "https://files.pythonhosted.org/packages/fb/81/f1459415cdb6c10d46942779de39bb59a77b38e5a76bb1def9227962eb45/propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f", size = 245150, upload-time = "2026-09-16T00:16:13.596Z" }, { url = "https://files.pythonhosted.org/packages/ce/4e/58b9b1460afc97a4c0b17ee89af701c4011d4d7f46470eba3aaff76a8069/propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c", size = 232166, upload-time = "2026-09-16T00:16:15.126Z" }, { url = "https://files.pythonhosted.org/packages/b9/c6/5a79e0eda3e7b6987d03d8c622ff6d52a42165a12e8418eb37694b9cc4b4/propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c", size = 206085, upload-time = "2026-09-16T00:16:16.713Z" }, { url = "https://files.pythonhosted.org/packages/4e/72/940aed42c73f9da345ca2de0f6e835c726498159abca5f1ef14fb0a2af8a/propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162", size = 228460, upload-time = "2026-09-16T00:16:18.352Z" }, { url = "https://files.pythonhosted.org/packages/85/71/3f54e1535c8f323d91ba566044d7c2b39ff6f6a2f1d0bd9071779d07b9b3/propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a", size = 218350, upload-time = "2026-09-16T00:16:20.064Z" }, { url = "https://files.pythonhosted.org/packages/a8/f4/025890cc389ac3ec485ecec607d4a7ca47e15bfa2a465746ab98af602536/propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826", size = 233156, upload-time = "2026-09-16T00:16:21.624Z" }, { url = "https://files.pythonhosted.org/packages/04/29/b39cae08c87c140d3d274f0a2c058cb5588e836175c3309e260b230ab07d/propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831", size = 206206, upload-time = "2026-09-16T00:16:23.204Z" }, { url = "https://files.pythonhosted.org/packages/18/61/e16462ef18a87247dc9ebbd5c606f46d5ce67e708bd9cc734dd0d9222564/propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c", size = 234469, upload-time = "2026-09-16T00:16:24.841Z" }, { url = "https://files.pythonhosted.org/packages/9f/84/b6a1490922427204fc47df920ed002eec709621de6b79b11592bf45c623a/propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38", size = 227311, upload-time = "2026-09-16T00:16:26.549Z" }, { url = "https://files.pythonhosted.org/packages/ff/5c/5a59527582e9bcb694b2f08b9894134b65a0f5f79dbff174f054f5f74ed0/propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123", size = 43512, upload-time = "2026-09-16T00:16:28.313Z" }, { url = "https://files.pythonhosted.org/packages/26/07/93cf699ed363681e754d7c3fad587fb09ef6b65618ee193332ad16a68d7b/propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3", size = 46264, upload-time = "2026-09-16T00:16:29.751Z" }, { url = "https://files.pythonhosted.org/packages/65/10/fef04fbdcd44a4a163cb5ff5674599c6d6fdefd64a5a459438f9ad2ba042/propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30", size = 43395, upload-time = "2026-09-16T00:16:31.5Z" }, { url = "https://files.pythonhosted.org/packages/70/f6/7e2f4dab0b92ab46111bd48cee9ee1e5f519514c44e3779ede5358d7ada0/propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f", size = 89825, upload-time = "2026-09-16T00:16:43.115Z" }, { url = "https://files.pythonhosted.org/packages/9f/8b/dfeff925cb6ced97ede701d5c6a99998da963c6f2e06abbf879c9dac5b54/propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3", size = 52159, upload-time = "2026-09-16T00:16:44.754Z" }, { url = "https://files.pythonhosted.org/packages/24/6c/924c810be5b7cf218ef47e707cf06d34adb4e3f3a31e3c24c55c6d945a88/propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68", size = 53956, upload-time = "2026-09-16T00:16:46.762Z" }, { url = "https://files.pythonhosted.org/packages/3f/b6/9ed0a5c939b58b6bed740a05b5d0f919f0b318d03284b4b6d81a0fe8a29a/propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c", size = 295235, upload-time = "2026-09-16T00:16:48.577Z" }, { url = "https://files.pythonhosted.org/packages/5a/eb/5ce886e902a2e781dddf110993d5329458a9b1a8626b876c65e5e25bf413/propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589", size = 294463, upload-time = "2026-09-16T00:16:50.539Z" }, { url = "https://files.pythonhosted.org/packages/f2/88/c98f49183ecd3e5b204a556f0ca47baa02c2206a500fe8c7ec1726297b0a/propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62", size = 300081, upload-time = "2026-09-16T00:16:52.423Z" }, { url = "https://files.pythonhosted.org/packages/27/0d/c5090f9e6f67cbc30a2b744c7bb0f8006dcba5ec1b0d82f866ae1cc7c5c4/propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72", size = 285360, upload-time = "2026-09-16T00:16:54.141Z" }, { url = "https://files.pythonhosted.org/packages/ac/9c/34a55396910583ed07926669ab309dde2213a2dec05a7e946bb90ad66908/propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9", size = 248014, upload-time = "2026-09-16T00:16:56.062Z" }, { url = "https://files.pythonhosted.org/packages/cd/b5/c0a142b656093ca397039dd3fe166cbb87c945712b534546514a24cd2611/propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece", size = 280662, upload-time = "2026-09-16T00:16:58.044Z" }, { url = "https://files.pythonhosted.org/packages/54/28/fab2809c2e337fe26becea9648e84d5cef46075c91b826acb13e4f9dd04e/propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a", size = 266149, upload-time = "2026-09-16T00:16:59.702Z" }, { url = "https://files.pythonhosted.org/packages/3a/11/7ddf336288b2678a5f054f8da2e2bd1a719f5d4b7de714d9c6bd588a2313/propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2", size = 283097, upload-time = "2026-09-16T00:17:01.459Z" }, { url = "https://files.pythonhosted.org/packages/1a/ae/351b1a5225f5473c411d9a612a229ae147cf0cf65c72ad838b87219ea8e8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b", size = 248160, upload-time = "2026-09-16T00:17:03.298Z" }, { url = "https://files.pythonhosted.org/packages/3f/d0/7f79f061e30d135bb615c9782c94a74652033d00b49254edbbf35a9165a8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284", size = 283036, upload-time = "2026-09-16T00:17:05.238Z" }, { url = "https://files.pythonhosted.org/packages/53/3c/016f1cad8bf4c428d748cf399b2bac603026fbfd6966e6a5579b5c5b6956/propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad", size = 279350, upload-time = "2026-09-16T00:17:06.881Z" }, { url = "https://files.pythonhosted.org/packages/ea/60/d8f72cb24b412487ed4c397f539117d3b74c3c33dd32020e91fe00a958a8/propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec", size = 45874, upload-time = "2026-09-16T00:17:08.567Z" }, { url = "https://files.pythonhosted.org/packages/d4/ef/8bae0a316d406644450522f2f3d44a4e19632f5f3bb60d1d0e6c53842616/propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432", size = 48574, upload-time = "2026-09-16T00:17:10.077Z" }, { url = "https://files.pythonhosted.org/packages/57/be/bcc053f66a97355683884b448198e79580fae8e8fa4d96b9bb01614e9913/propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e", size = 45625, upload-time = "2026-09-16T00:17:11.377Z" }, { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, ] [[package]] name = "pygments" version = "2.21.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] name = "pytest" version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "ruff" version = "0.16.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" }, { url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" }, { url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" }, { url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" }, { url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" }, { url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" }, { url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" }, { url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" }, { url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" }, { url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" }, { url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" }, { url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" }, { url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" }, { url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" }, { url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" }, { url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" }, { url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "tomli" version = "2.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] name = "ty" version = "0.0.84" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e1/f6/34f8869c45ea87fe6a931ffa824b1fe4428167173543d92988d631add355/ty-0.0.84.tar.gz", hash = "sha256:0cefdb0cd5d399418dbe83c349ba605047b7dff815ae6f460c80e8522b40be67", size = 7409537, upload-time = "2026-09-24T13:16:24.367Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/aa/0c/600648778e1c79133ce6b6a5c4c2531ae16b90c9b30f5041b38b574f61b8/ty-0.0.84-py3-none-linux_armv6l.whl", hash = "sha256:868438e2b9abfc835dd5b5b07ed693db95d8348ac40e3228252511d63ae87695", size = 13935333, upload-time = "2026-09-24T13:15:31.167Z" }, { url = "https://files.pythonhosted.org/packages/c5/3b/27f06f49945c36170765fad82076898039ae3873338e9865f876dd62f12c/ty-0.0.84-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b00f39b521f0b559cde0f6abf8dd47213b513b04f33d58efeb9b98486a16bb17", size = 13511328, upload-time = "2026-09-24T13:15:34.446Z" }, { url = "https://files.pythonhosted.org/packages/fc/b6/b12dd130e7f30141f0645614f454c925991cd389b1cc6c8284fd6bf32326/ty-0.0.84-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8f78c2915567f2ee62ce0c24d86091bc76f81577677755e242d09951db588b9a", size = 13445445, upload-time = "2026-09-24T13:15:37.476Z" }, { url = "https://files.pythonhosted.org/packages/44/96/f012de4b3c1d9a4773326621380d768b4d48c47e620dc012775c5e01c7d7/ty-0.0.84-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61155cad0921e890d86d1d911a40b3a0e924ef4bdc3b4f3865a6c89c7e3012dc", size = 13471235, upload-time = "2026-09-24T13:15:40.64Z" }, { url = "https://files.pythonhosted.org/packages/05/32/dca630f5f5353fa7bf67eebccf1549df99b16656d21071d0df6677c826a5/ty-0.0.84-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:417c3bf14947ed16b0b92976ab333c27915590354e96d4f581709068d3811355", size = 13680704, upload-time = "2026-09-24T13:15:43.817Z" }, { url = "https://files.pythonhosted.org/packages/44/8d/33702c8f62f05ad45b33644fb914d483de6a0a7bc8e51884e9b8773e6d69/ty-0.0.84-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e06ef117759f654f5379efe9ae79bda8a186016c016f1023d05c34f8e15c1546", size = 14518923, upload-time = "2026-09-24T13:15:46.802Z" }, { url = "https://files.pythonhosted.org/packages/c8/a1/da3246e6ef2ae0e842bca4058b25c54bee55ab058ffff4ee54e72517ec01/ty-0.0.84-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a05d1d53db8b64410ab38c6e32c5898510584869d1d5dd9c0605db81c9aef7df", size = 15027731, upload-time = "2026-09-24T13:15:49.755Z" }, { url = "https://files.pythonhosted.org/packages/20/17/b0099098a560aeb4f9fde6c912f9a81622c3e44e6555e1fbe322b368ec31/ty-0.0.84-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92400494ab855cfb1204ac1101e3f53b2d09a8e21d171758b2cf9acdab1809e", size = 14748260, upload-time = "2026-09-24T13:15:52.713Z" }, { url = "https://files.pythonhosted.org/packages/60/d5/32cd67aedb271f006e716ad093806c96c5d7e2af86be8255469dbd3d76c8/ty-0.0.84-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af17eb391ae3027fed9be1780bea555c2b8a25ba68e184c627b4981b2c9eaab7", size = 14205858, upload-time = "2026-09-24T13:15:55.677Z" }, { url = "https://files.pythonhosted.org/packages/43/c4/638ac62ca2e8eca1b92e3e16273085744fada728b993500819b5c258ca15/ty-0.0.84-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:44cf06af0e7ec78ea6d8ff33a3450fc19627756a4fba681f7410d4e3ef63047a", size = 14573196, upload-time = "2026-09-24T13:15:59.165Z" }, { url = "https://files.pythonhosted.org/packages/a6/f6/9ee81c5d8921e5fd5f10889ae563c95cd3272b31df2ada528919ca1cef6b/ty-0.0.84-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c6e56b443ff9ba462e34bfbc1e582004c2719ed35f8f93a433744b0ba0afb2d9", size = 13480760, upload-time = "2026-09-24T13:16:02.313Z" }, { url = "https://files.pythonhosted.org/packages/f6/83/fbe5d1177667ae16d84c3242846b143809c9cc09568b333e5a9b4d5908f6/ty-0.0.84-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b6562fcc48820c1030e2a4509ef2c14ef93043ca4860280074314ac3a825b95", size = 13696346, upload-time = "2026-09-24T13:16:05.391Z" }, { url = "https://files.pythonhosted.org/packages/86/9a/1b4ac9ddc34cc0ab4624bc219825d66af52f1ad87f4778ad08ad03fa06d5/ty-0.0.84-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dee7421451efcbd70f03e95a8f2c8d9e5ec3d35edcd27b4095ee00ee062abfc4", size = 13959410, upload-time = "2026-09-24T13:16:08.661Z" }, { url = "https://files.pythonhosted.org/packages/c9/04/70e289437a67dfd686056ab4fd91880261a8233033a6a95d29393741fb97/ty-0.0.84-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:31e96307cf621babe969307dd657783d672e2d8bdbd06493710da635bf63fa07", size = 14350294, upload-time = "2026-09-24T13:16:11.865Z" }, { url = "https://files.pythonhosted.org/packages/9c/f4/ae7dbd118425c850981d23efb850f0d9d7f6b8d2b852bde2767434f668b4/ty-0.0.84-py3-none-win32.whl", hash = "sha256:4bdc85f91cbaae35825f2b17bb4ac5de0563367875cb0f42b2bf07a1bf7ff2ba", size = 13176439, upload-time = "2026-09-24T13:16:15.049Z" }, { url = "https://files.pythonhosted.org/packages/29/ec/994d87252f065dda5a500bc8afc726d18f6582d81d6f4ff1feb8a267c800/ty-0.0.84-py3-none-win_amd64.whl", hash = "sha256:71cc9aa7a7c89d4e12c6a814e9c0c3310ad5bee44efedbd6f5a42d91e9ac0f0d", size = 13962879, upload-time = "2026-09-24T13:16:18.191Z" }, { url = "https://files.pythonhosted.org/packages/6d/1c/e6e91852b72566b1ab9aca504712857dfb89324c12092a5bd61d42d34427/ty-0.0.84-py3-none-win_arm64.whl", hash = "sha256:6c9d3718c4a0d318bc9c92af9b2aa85c3294e7dff54cf1d69ad79568f56516da", size = 13656453, upload-time = "2026-09-24T13:16:21.51Z" }, ] [[package]] name = "typer" version = "0.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "rich" }, { name = "shellingham" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" }, ] [[package]] name = "urllib3" version = "2.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] name = "uvloop" version = "0.23.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fa/42/02c739ce85fb2ee8d99212c61417da8140c6b87e9d97c430bea520d76044/uvloop-0.23.0.tar.gz", hash = "sha256:28d160f51ab4da3b187063652e643dea6831072add4adc1e6d62afbe73b6be27", size = 2559185, upload-time = "2026-10-01T03:17:04.4Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2f/b1/948067eab45d5307f04b34e50eb7bd1f7352aee866fa5f0706b061ddacf0/uvloop-0.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:24c58ae4a83e93a04c504bcc678125e36a0bfc44af928ad69444880c60f187a5", size = 1415276, upload-time = "2026-10-01T03:15:32.634Z" }, { url = "https://files.pythonhosted.org/packages/8a/6f/ee3ee84c5d27f2f0a47ae8b67a6adeacf9841b193c0e07412a1403586ce2/uvloop-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0efdd55bddbd36bb2fcb842d64c0d5f6407c6958c68088cc25df8c09edc5b5fd", size = 779533, upload-time = "2026-10-01T03:15:34.062Z" }, { url = "https://files.pythonhosted.org/packages/25/0d/b5f69dae3736d96a8753c6ecd32d676ecd212be7ba3252e9c379ad9cc05c/uvloop-0.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fcd721113260ffb5e38bf14a8725b17d431f34209f7d1c7005b667946e630b3", size = 3896377, upload-time = "2026-10-01T03:15:35.816Z" }, { url = "https://files.pythonhosted.org/packages/16/fd/8cbf6124607863399008ae4b0d2bb50c22ed83526deec28dca08d635eb6d/uvloop-0.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab17b3a8aa754be0de0e397f7b95f13b14e56f077a4c6ae295e3d4afd199b325", size = 3956355, upload-time = "2026-10-01T03:15:37.688Z" }, { url = "https://files.pythonhosted.org/packages/a7/7a/b73007866e7198519067a1f1afc343b4973ae924d2b7afcea67c44320a98/uvloop-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:80cac5cb90ed7b9b72a217a1d6982b15b829cdbd0ee6bc19b93e3a9e47fb0ac9", size = 3755618, upload-time = "2026-10-01T03:15:39.27Z" }, { url = "https://files.pythonhosted.org/packages/3c/28/e50816f1ce38b97b28d62bc4adf7c82c33b7c68fa902e41a39adc8a3d189/uvloop-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:93087a845cdfb35753e539354ac9551bdd2ff528c202a98df0ae46e852bcf021", size = 3863192, upload-time = "2026-10-01T03:15:40.882Z" }, { url = "https://files.pythonhosted.org/packages/05/98/04e766a6de99e6f7f955ecb7829e8d5a557de3427cb85be2236de54dda0c/uvloop-0.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:93935ab27b6eaef4c3e5489aebc84284f0644592f7ab516df60ee1b27eaf5eb3", size = 1393055, upload-time = "2026-10-01T03:15:42.526Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/499e7b863a848ede009539bce39806b66205da5f8779354228e785601144/uvloop-0.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4448e9124537620f9c25d004c227bb5104440b58955c19bbd312d910af919a63", size = 768909, upload-time = "2026-10-01T03:15:43.974Z" }, { url = "https://files.pythonhosted.org/packages/3d/95/a880f8ce3b87ac5b307c354e8ee480be4658d24bf01f87921d57e3530b4a/uvloop-0.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7548ede3ee908cfabc0d068106e303a9a2d811af959cdf6ab85676344cedcda", size = 4419106, upload-time = "2026-10-01T03:15:45.551Z" }, { url = "https://files.pythonhosted.org/packages/51/27/c1d2f9fa977f8f42ea294604166df10e0027e6dc6cd17f85ede386c9bf36/uvloop-0.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:090865d8ce7a03986755a3ce711b7dd0d4b44eb14ab74368b717f3fad1180208", size = 4532597, upload-time = "2026-10-01T03:15:47.258Z" }, { url = "https://files.pythonhosted.org/packages/42/dd/2cb6a2c8a30ca55c07a882dd4ae4ceae0fa7d8c15b25b3b7cb9a4b6cf4ca/uvloop-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd6f2f81c7b9da99d301c0b16b82044e76fe887086e42e1590ecf520b94dbdac", size = 4230048, upload-time = "2026-10-01T03:15:49.119Z" }, { url = "https://files.pythonhosted.org/packages/f4/52/29989cbaa4022dc4ef35c1dd60a4ab989e4c2065f341ed483ae71d2bd950/uvloop-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a6ac96da66c35bf789bdcde78a88dc7d56b7907d8379648c54adc1c61594575d", size = 4394152, upload-time = "2026-10-01T03:15:50.829Z" }, { url = "https://files.pythonhosted.org/packages/5f/83/eb980d64e6dd5da46d4dc35755fa6afd6b5b47141437cf89615f1117c5a6/uvloop-0.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2dcff2d69be43e6559e5dad2c5a7a2dbfb60e05a77311b6c4b7a4a8123d86c65", size = 1412726, upload-time = "2026-10-01T03:15:52.49Z" }, { url = "https://files.pythonhosted.org/packages/04/c1/02a725e7698134c647904bdee6589e2be14a0e7fc9942c74f86e2b90d48b/uvloop-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19c64108b507cd0bc140e400e3396bacebd9d504956aa7726272bf6de7d9aabb", size = 779071, upload-time = "2026-10-01T03:15:54.02Z" }, { url = "https://files.pythonhosted.org/packages/0b/1d/cde53c79e8c01884ad1cdca8e407e086d523362cfe4139e2c2a8dde27304/uvloop-0.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1748321e3c59a14a75404b1ae8d5a8d81c4e201803ea0e14c1b6fd84421024b5", size = 4395323, upload-time = "2026-10-01T03:15:55.549Z" }, { url = "https://files.pythonhosted.org/packages/98/54/b12915bebbf99d7ae0796211e7f5977b95f069830dca45dc1a346d84125d/uvloop-0.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2cba180d6451822763eda8364f342435a873bcfb3849cbd82fdeca248ca65eb", size = 4480449, upload-time = "2026-10-01T03:15:57.362Z" }, { url = "https://files.pythonhosted.org/packages/f7/8e/da6de68c31549a052a105fc76f5a9a204f6df22cb0909440aa4dbb06f9a2/uvloop-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dc61e4f9e37b507069dc7e659ae28bca7adcb04c993c3508214315d12c63f848", size = 4219177, upload-time = "2026-10-01T03:15:59.351Z" }, { url = "https://files.pythonhosted.org/packages/a1/c3/1b53c6a89dc9c9d5cb75eb9a0b891ad69b32e1421ad3aa01617a9cbdcc78/uvloop-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7337b06a9f9ed9ea3049f04b76f65819db9b19bb832ee598e97b388eadf25e5f", size = 4346132, upload-time = "2026-10-01T03:16:01.064Z" }, { url = "https://files.pythonhosted.org/packages/4e/a4/00e85345871c59c834a23c136c1771205856028ecc8ba940b3951178e59b/uvloop-0.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b90397a50ad6332ed3e459c648ac20d182cce24a557354363ad85fc9ea4a17cd", size = 1421363, upload-time = "2026-10-01T03:16:02.599Z" }, { url = "https://files.pythonhosted.org/packages/d0/a9/e5f0f3cfde30af3ec32eba8ec07bccdba2b5116afbd1ecc53edfeb0a0790/uvloop-0.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:be53e1d5f83de43dc175c87612ecc128d444b38e5c56cb3f807f5a73d6887476", size = 785177, upload-time = "2026-10-01T03:16:04.018Z" }, { url = "https://files.pythonhosted.org/packages/9e/79/9ddf78f8cd75a15c14a09a57f59c587b8cd9d82802c5c8368b9c3ebefa0b/uvloop-0.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b3cbc4f96ddfa1fb88a78a69dd851369825b7816d9702eee8c4461505ba172e", size = 4381060, upload-time = "2026-10-01T03:16:05.642Z" }, { url = "https://files.pythonhosted.org/packages/1e/20/57d63c44d32326878fcad5c63854afc9deb394ed95673c1b1a429178c79d/uvloop-0.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31e0cf90bc8fd88784f6802cdba968a51fb1aec1cc3feec74d862b2d371d1330", size = 4418891, upload-time = "2026-10-01T03:16:07.326Z" }, { url = "https://files.pythonhosted.org/packages/12/c5/0795abecda2cc3dfe41033f880a32a9ff103be4e6b177ac736833c153a0e/uvloop-0.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8ed556fcc87a4091cf61587ef172fa104323dc89ecc085a618ba7ff8629a8f", size = 4214811, upload-time = "2026-10-01T03:16:09.13Z" }, { url = "https://files.pythonhosted.org/packages/20/18/9010dacd5221eec1bd79a4a83ac68f3db6a42d7bb657f7b640c4838ca6b6/uvloop-0.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3fbfe82829d8e381426a289b87e59e585278728361db9ce975b88b51f64f410", size = 4294876, upload-time = "2026-10-01T03:16:10.875Z" }, { url = "https://files.pythonhosted.org/packages/b1/08/f6384a03c771d00067cba4f542a69b2fc1a982e9fd78b357c2f788678d72/uvloop-0.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7e35c9bc977760981693e1a7a51493b58ee5a501f9ebb1e547565ee40b6c6208", size = 1494811, upload-time = "2026-10-01T03:16:12.399Z" }, { url = "https://files.pythonhosted.org/packages/ac/01/756a4fb24a449f313cf4a153eb0c6210b49cfe5539255ec9fb1e17d2c4ef/uvloop-0.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5bb9be71d9ee39b4359b832f9569518ec9bc08704194034e79e4958e6bc4d46d", size = 819396, upload-time = "2026-10-01T03:16:14.094Z" }, { url = "https://files.pythonhosted.org/packages/3e/45/e314b0c600b14f53dad3a3c2d7a922a249a88225fd727652b53e1854b9dd/uvloop-0.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e84575f11873c109cf3962ad0bdf679094466184125f4cadcc41a73febff41f", size = 4734966, upload-time = "2026-10-01T03:16:15.815Z" }, { url = "https://files.pythonhosted.org/packages/66/0d/8686a7f0b1b2d55ebd770ba21f8e0e4ffa0cde5ab738f43ffb8264499052/uvloop-0.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbbdb8fcd5e7062e546eec1ac78c28bb21ae7df54c18f8e4b06e15a18d661a49", size = 4584963, upload-time = "2026-10-01T03:16:18.198Z" }, { url = "https://files.pythonhosted.org/packages/78/b2/034a2d47e435ac02357c42956246887167bdc0357bdd6ad31c5f6d94497b/uvloop-0.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76345f51367fb1f23e08605c6efb18374f669be5b223658fbab6b17627950507", size = 4421388, upload-time = "2026-10-01T03:16:19.953Z" }, { url = "https://files.pythonhosted.org/packages/f0/77/131f4b583e6b4b715c404a66b51c812d701db20f25c9018b188a2b00062c/uvloop-0.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7ef4701a96553514b2688e342ef1bf2beae6cfd172d89a76c768292aabf405", size = 4402414, upload-time = "2026-10-01T03:16:21.716Z" }, { url = "https://files.pythonhosted.org/packages/58/3d/ee11f4718ea1280595c67ed25c83d4c92115dc100bbdfd192d3ed9339168/uvloop-0.23.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:f1341c6abcee1c31277cfe28d34e46196f2143ec3d755e6efe7452126e1f626d", size = 1418095, upload-time = "2026-10-01T03:16:23.241Z" }, { url = "https://files.pythonhosted.org/packages/f8/0c/7ca516a0671418517d79a09d3ff2ccbb44af94c75711afa6e4cf58aa6f65/uvloop-0.23.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e095f9e105af76593b4c183bb0bcbdae64bd913a59ec595732dc108b48730ab5", size = 784837, upload-time = "2026-10-01T03:16:24.666Z" }, { url = "https://files.pythonhosted.org/packages/35/95/75d4e28e596d505b7ae11de517646b4ca3d369fb8537ba755410380da11a/uvloop-0.23.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f673d835bdb1a60229cc3609a113fd2c9ce3f4a3c75ad4eaed111180c00199d2", size = 4380276, upload-time = "2026-10-01T03:16:26.389Z" }, { url = "https://files.pythonhosted.org/packages/10/99/68daf827ad62efaf4667d1f3fda127046d42161178396bdd93aab3684082/uvloop-0.23.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f23f403a273900d57de6ee5ca0614c650f7f58563065dad1a4744498960e53", size = 4451496, upload-time = "2026-10-01T03:16:28.364Z" }, { url = "https://files.pythonhosted.org/packages/71/69/f67e696ee688f426a96f99099bae26fec14a1d0fa75dccdd6518ee267c0c/uvloop-0.23.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cbe8d03d4efcccdb7fcedecbaa1e1fa02913eaf3a74cb933634a6bc6d2ea9e2a", size = 4212541, upload-time = "2026-10-01T03:16:30.014Z" }, { url = "https://files.pythonhosted.org/packages/f1/6a/c8c436a9d7453297b4be70bdf6a9f9fc9400da45e0059ddf7b28ab63f4c7/uvloop-0.23.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:4f1798f56c6f4ba5ac11fa2869e5717926e4470d97a1dd42b4f59219d43b5027", size = 4319377, upload-time = "2026-10-01T03:16:31.705Z" }, { url = "https://files.pythonhosted.org/packages/3b/2c/8fc15a03489299aab8a6212dfe0f137dc39836f915c87f7fd9d9ddd814de/uvloop-0.23.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:098a85e1393ef5202767b7e5fb41a32cd8bd81e6ee4af364c179801c4aa3f6d4", size = 1493428, upload-time = "2026-10-01T03:16:33.859Z" }, { url = "https://files.pythonhosted.org/packages/b7/7c/05e4a210790229607f71460fcb2ed4a2c7bc72668d8a928ce577c22e38f8/uvloop-0.23.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:5a2bbad3a63007f7e9524d4903ba04fee252557c2acd86f9a3d4f91786695254", size = 818115, upload-time = "2026-10-01T03:16:35.45Z" }, { url = "https://files.pythonhosted.org/packages/65/14/a40b11c6c024213803b13955664a15754c72f64c873a33d986b26ec9ff5b/uvloop-0.23.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a08875543bbd4519faf30497506c9cda8a48470467ffdf967c7313c7a5981a8", size = 4734149, upload-time = "2026-10-01T03:16:37.025Z" }, { url = "https://files.pythonhosted.org/packages/9f/83/f421a077712c1e87603bfec62744c3cd3a2f4b47378025db3d740df9af0d/uvloop-0.23.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12634f15e6625f78b3f2922f91404c4d7173487eba11746764153f556e9852dc", size = 4661763, upload-time = "2026-10-01T03:16:38.719Z" }, { url = "https://files.pythonhosted.org/packages/f5/62/25dcaa6b7e7b48f82ce633854ce96597ab768f9650931f4f86c572de392c/uvloop-0.23.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:378188efbb1524f2219d05246a3e1e5907217848d2882144dff59585f1b81d55", size = 4421324, upload-time = "2026-10-01T03:16:40.488Z" }, { url = "https://files.pythonhosted.org/packages/05/46/04628239b43dcef703af314202a3307d6060918e2d76aa86c5b1188f5551/uvloop-0.23.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4b8e207c67d207a8608fec57e116511030af3495dc0109b8c333cf9cb412b16f", size = 4462501, upload-time = "2026-10-01T03:16:42.359Z" }, ] [[package]] name = "yarl" version = "1.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" }, { url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" }, { url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" }, { url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" }, { url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" }, { url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" }, { url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" }, { url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" }, { url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" }, { url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" }, { url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" }, { url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" }, { url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" }, { url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" }, { url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" }, { url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" }, { url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" }, { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, ]