pax_global_header00006660000000000000000000000064150537303750014522gustar00rootroot0000000000000052 comment=5147b7dab326d6a9ddf69e54d08f8104db46f82a qasync-0.28.0/000077500000000000000000000000001505373037500131075ustar00rootroot00000000000000qasync-0.28.0/.github/000077500000000000000000000000001505373037500144475ustar00rootroot00000000000000qasync-0.28.0/.github/dependabot.yml000066400000000000000000000004371505373037500173030ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "monthly" # disable version updates for dependencies open-pull-requests-limit: 0 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" qasync-0.28.0/.github/workflows/000077500000000000000000000000001505373037500165045ustar00rootroot00000000000000qasync-0.28.0/.github/workflows/coverage.yml000066400000000000000000000022631505373037500210250ustar00rootroot00000000000000name: Coverage on: workflow_run: workflows: [Tests] types: - completed jobs: report: name: report coverage runs-on: ubuntu-latest if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' permissions: # Gives the action the necessary permissions for publishing new # comments in pull requests. pull-requests: write # Gives the action the necessary permissions for editing existing # comments (to avoid publishing multiple comments in the same PR) contents: write # Gives the action the necessary permissions for looking up the # workflow that launched this workflow, and download the related # artifact that contains the comment to be published actions: read steps: # DO NOT run actions/checkout here, for security reasons # For details, refer to https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ - name: Post comment uses: py-cov-action/python-coverage-comment-action@v3.35 with: GITHUB_TOKEN: ${{ github.token }} GITHUB_PR_RUN_ID: ${{ github.event.workflow_run.id }} qasync-0.28.0/.github/workflows/main.yml000066400000000000000000000075141505373037500201620ustar00rootroot00000000000000name: Tests on: push: branches: - master - develop pull_request: branches: - master - develop concurrency: group: tests-${{ github.head_ref || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: tests: name: ${{ matrix.os }} / ${{ matrix.python-version }} / ${{ matrix.qt-version }} runs-on: ${{ matrix.image }} strategy: matrix: os: [ubuntu, windows, macos-x86_64, macos-arm64] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] qt-version: ["pyside2", "pyside6", "pyqt5", "pyqt6"] include: - os: ubuntu image: ubuntu-24.04 - os: windows image: windows-2022 - os: macos-x86_64 image: macos-13 - os: macos-arm64 image: macos-14 exclude: # pyside2 does not publish arm64 packages - os: macos-arm64 qt-version: pyside2 # pyside2 requires python <3.11 - python-version: "3.11" qt-version: pyside2 - python-version: "3.12" qt-version: pyside2 - python-version: "3.13" qt-version: pyside2 # pyside6 and pyqt6 require python >=3.9 - python-version: "3.8" qt-version: pyside6 - python-version: "3.8" qt-version: pyqt6 fail-fast: false defaults: run: shell: bash steps: - name: Checkout uses: actions/checkout@v5 - name: Install libxcb dependencies if: ${{ matrix.os == 'ubuntu' }} env: DEBIAN_FRONTEND: noninteractive run: | sudo apt-get -qq update sudo apt-get -qq install '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libegl-dev - name: Setup python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.8.3" enable-cache: true - name: Install qasync run: uv sync --locked --group dev - name: Install qt ${{ matrix.qt-version }} run: uv pip install ${{ matrix.qt-version }} - name: Run tests uses: coactions/setup-xvfb@v1 env: QT_API: "${{ matrix.qt_version }}" COVERAGE_FILE: ".coverage.${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.qt-version }}" with: run: uv run coverage run --context=${{matrix.qt-version}} - name: Upload coverage artifacts uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.qt-version }} path: .coverage.${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.qt-version }} include-hidden-files: true coverage: name: collect coverage runs-on: ubuntu-latest needs: tests permissions: pull-requests: write contents: write steps: - name: Checkout uses: actions/checkout@v5 - name: Download coverage artifacts uses: actions/download-artifact@v4 with: pattern: coverage-* merge-multiple: true - name: Coverage comment id: coverage_comment uses: py-cov-action/python-coverage-comment-action@v3.35 with: GITHUB_TOKEN: ${{ github.token }} MERGE_COVERAGE_FILES: true ANNOTATE_MISSING_LINES: true - name: Store coverage comment to be posted uses: actions/upload-artifact@v4 if: steps.coverage_comment.outputs.COMMENT_FILE_WRITTEN == 'true' with: name: python-coverage-comment-action path: python-coverage-comment-action.txt qasync-0.28.0/.github/workflows/release.yml000066400000000000000000000015121505373037500206460ustar00rootroot00000000000000name: Release on: push: tags: - "*.*.*" jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v5 - name: Set up python uses: actions/setup-python@v5 with: python-version-file: pyproject.toml - name: Install uv uses: astral-sh/setup-uv@v6 with: version: "0.8.3" enable-cache: true - name: Build qasync run: uv build - name: Create release uses: ncipollo/release-action@v1 with: artifacts: "dist/*" allowUpdates: true generateReleaseNotes: true token: ${{ github.token }} - name: Publish to PyPI env: UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} run: uv publish qasync-0.28.0/.gitignore000066400000000000000000000004131505373037500150750ustar00rootroot00000000000000# editors .idea/* .vscode/* .python-version .mise.toml .DS_Store # python .venv __pycache__/ *.py[cod] *$py.class .mypy_cache # packaging *.egg !/tests/**/*.egg /*.egg-info /dist/* build _build .cache *.so # testing / coverage .coverage* .pytest_cache .ruff_cache qasync-0.28.0/.pre-commit-config.yaml000066400000000000000000000011751505373037500173740ustar00rootroot00000000000000default_stages: - pre-commit - pre-push repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. rev: 0.8.3 hooks: - id: uv-lock - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.12.5 hooks: # Run the linter. - id: ruff args: [--fix] # Run the formatter. - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-yaml - id: check-toml - id: end-of-file-fixer exclude_types: [json] - id: trailing-whitespace exclude_types: [json] qasync-0.28.0/LICENSE000066400000000000000000000025541505373037500141220ustar00rootroot00000000000000Copyright (c) 2019, Sam McCormack Copyright (c) 2018, Gerard Marull-Paretas Copyright (c) 2014-2018, Mark Harviston, Arve Knudsen All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. qasync-0.28.0/README.md000066400000000000000000000061641505373037500143750ustar00rootroot00000000000000# qasync [![Maintenance](https://img.shields.io/maintenance/yes/2025)](https://pypi.org/project/qasync) [![PyPI](https://img.shields.io/pypi/v/qasync)](https://pypi.org/project/qasync) [![PyPI - License](https://img.shields.io/pypi/l/qasync)](/LICENSE) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/qasync)](https://pypi.org/project/qasync) [![PyPI - Download](https://img.shields.io/pypi/dm/qasync)](https://pypi.org/project/qasync) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/CabbageDevelopment/qasync/main.yml)](https://github.com/CabbageDevelopment/qasync/actions/workflows/main.yml) ## Introduction `qasync` allows coroutines to be used in PyQt/PySide applications by providing an implementation of the `PEP 3156` event loop. With `qasync`, you can use `asyncio` functionalities directly inside Qt app's event loop, in the main thread. Using async functions for Python tasks can be much easier and cleaner than using `threading.Thread` or `QThread`. If you need some CPU-intensive tasks to be executed in parallel, `qasync` also got that covered, providing `QEventLoop.run_in_executor` which is functionally identical to that of `asyncio`. By default `QThreadExecutor` is used, but any class implementing the `concurrent.futures.Executor` interface will do the job. ### Basic Example ```python import asyncio import sys from PySide6.QtWidgets import QVBoxLayout, QWidget from qasync import QApplication, QEventLoop class MainWindow(QWidget): def __init__(self): super().__init__() self.setLayout(QVBoxLayout()) self.lbl_status = QLabel("Idle", self) self.layout().addWidget(self.lbl_status) @asyncClose async def closeEvent(self, event): pass @asyncSlot() async def onMyEvent(self): pass if __name__ == "__main__": app = QApplication(sys.argv) app_close_event = asyncio.Event() app.aboutToQuit.connect(app_close_event.set) main_window = MainWindow() main_window.show() # for 3.11 or older use qasync.run instead of asyncio.run # qasync.run(app_close_event.wait()) asyncio.run(app_close_event.wait(), loop_factory=QEventLoop) ``` More detailed examples can be found [here](https://github.com/CabbageDevelopment/qasync/tree/master/examples). ### The Future of `qasync` `qasync` is a fork of [asyncqt](https://github.com/gmarull/asyncqt), which is a fork of [quamash](https://github.com/harvimt/quamash). `qasync` was created because those are no longer maintained. May it live longer than its predecessors. **`qasync` will continue to be maintained, and will still be accepting pull requests.** ## Requirements - Python >=3.8, <3.14 - PyQt5/PyQt6 or PySide2/PySide6 `qasync` is tested on Ubuntu, Windows and MacOS. If you need Python 3.6 or 3.7 support, use the [v0.25.0](https://github.com/CabbageDevelopment/qasync/releases/tag/v0.25.0) tag/release. ## Installation To install `qasync`, use `pip`: ``` pip install qasync ``` ## License You may use, modify and redistribute this software under the terms of the [BSD License](http://opensource.org/licenses/BSD-2-Clause). See [LICENSE](/LICENSE). qasync-0.28.0/examples/000077500000000000000000000000001505373037500147255ustar00rootroot00000000000000qasync-0.28.0/examples/aiohttp_fetch.py000066400000000000000000000041731505373037500201250ustar00rootroot00000000000000import asyncio import sys import aiohttp # from PyQt6.QtWidgets import ( from PySide6.QtWidgets import ( QApplication, QLabel, QLineEdit, QPushButton, QTextEdit, QVBoxLayout, QWidget, ) from qasync import QEventLoop, asyncClose, asyncSlot class MainWindow(QWidget): """Main window.""" _DEF_URL: str = "https://jsonplaceholder.typicode.com/todos/1" """Default URL.""" def __init__(self): super().__init__() self.setLayout(QVBoxLayout()) self.lbl_status = QLabel("Idle", self) self.layout().addWidget(self.lbl_status) self.edit_url = QLineEdit(self._DEF_URL, self) self.layout().addWidget(self.edit_url) self.edit_response = QTextEdit("", self) self.layout().addWidget(self.edit_response) self.btn_fetch = QPushButton("Fetch", self) self.btn_fetch.clicked.connect(self.on_btn_fetch_clicked) self.layout().addWidget(self.btn_fetch) self.session: aiohttp.ClientSession @asyncClose async def closeEvent(self, event): # noqa:N802 await self.session.close() async def boot(self): self.session = aiohttp.ClientSession() @asyncSlot() async def on_btn_fetch_clicked(self): self.btn_fetch.setEnabled(False) self.lbl_status.setText("Fetching...") try: async with self.session.get(self.edit_url.text()) as r: self.edit_response.setText(await r.text()) except Exception as exc: self.lbl_status.setText("Error: {}".format(exc)) else: self.lbl_status.setText("Finished!") finally: self.btn_fetch.setEnabled(True) if __name__ == "__main__": app = QApplication(sys.argv) app_close_event = asyncio.Event() app.aboutToQuit.connect(app_close_event.set) main_window = MainWindow() main_window.show() async def async_main(): asyncio.create_task(main_window.boot()) await app_close_event.wait() # for 3.11 or older use qasync.run instead of asyncio.run # qasync.run(async_main()) asyncio.run(async_main(), loop_factory=QEventLoop) qasync-0.28.0/examples/executor_example.py000066400000000000000000000016571505373037500206610ustar00rootroot00000000000000import asyncio import functools import sys import time # from PyQt6.QtWidgets import from PySide6.QtWidgets import QApplication, QProgressBar from qasync import QEventLoop, QThreadExecutor async def master(): progress = QProgressBar() progress.setRange(0, 99) progress.show() await first_50(progress) loop = asyncio.get_running_loop() with QThreadExecutor(1) as exec: await loop.run_in_executor(exec, functools.partial(last_50, progress), loop) async def first_50(progress): for i in range(50): progress.setValue(i) await asyncio.sleep(0.1) def last_50(progress, loop): for i in range(50, 100): loop.call_soon_threadsafe(progress.setValue, i) time.sleep(0.1) if __name__ == "__main__": app = QApplication(sys.argv) # for 3.11 or older use qasync.run instead of asyncio.run # qasync.run(master()) asyncio.run(master(), loop_factory=QEventLoop) qasync-0.28.0/examples/modal_example.py000066400000000000000000000021531505373037500201070ustar00rootroot00000000000000import asyncio import sys # from PyQt6.QtWidgets import from PySide6.QtWidgets import QApplication, QMessageBox, QProgressBar from qasync import QEventLoop, asyncWrap async def master(): progress = QProgressBar() progress.setRange(0, 99) progress.show() await first_50(progress) async def first_50(progress): for i in range(50): progress.setValue(i) await asyncio.sleep(0.1) # Schedule the last 50% to run asynchronously asyncio.create_task(last_50(progress)) # create a notification box, use helper to make entering event loop safe. result = await asyncWrap( lambda: QMessageBox.information( None, "Task Completed", "The first 50% of the task is completed." ) ) assert result == QMessageBox.StandardButton.Ok async def last_50(progress): for i in range(50, 100): progress.setValue(i) await asyncio.sleep(0.1) if __name__ == "__main__": app = QApplication(sys.argv) event_loop = QEventLoop(app) asyncio.set_event_loop(event_loop) event_loop.run_until_complete(master()) event_loop.close() qasync-0.28.0/examples/qml_httpx/000077500000000000000000000000001505373037500167455ustar00rootroot00000000000000qasync-0.28.0/examples/qml_httpx/app.py000066400000000000000000000022771505373037500201070ustar00rootroot00000000000000import sys import asyncio from pathlib import Path from PySide6.QtCore import QUrl from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType from qasync import QEventLoop, QApplication from service import ExampleService QML_PATH = Path(__file__).parent.absolute().joinpath("qml") if __name__ == "__main__": app = QApplication(sys.argv) engine = QQmlApplicationEngine() engine.addImportPath(QML_PATH) app.aboutToQuit.connect(engine.deleteLater) engine.quit.connect(app.quit) # register our service, making it usable directly in QML qmlRegisterType(ExampleService, "qasync", 1, 0, ExampleService.__name__) # alternatively, instantiate the service and inject it into the QML engine # service = ExampleService() # engine.rootContext().setContextProperty("service", service) event_loop = QEventLoop(app) asyncio.set_event_loop(event_loop) app_close_event = asyncio.Event() app.aboutToQuit.connect(app_close_event.set) engine.quit.connect(app_close_event.set) qml_entry = QUrl.fromLocalFile(str(QML_PATH.joinpath("Main.qml"))) engine.load(qml_entry) with event_loop: event_loop.run_until_complete(app_close_event.wait()) qasync-0.28.0/examples/qml_httpx/qml/000077500000000000000000000000001505373037500175365ustar00rootroot00000000000000qasync-0.28.0/examples/qml_httpx/qml/Main.qml000066400000000000000000000004641505373037500211410ustar00rootroot00000000000000import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import QtQuick.Window 2.15 ApplicationWindow { id: root title: "qasync" visible: true width: 420 height: 240 Loader { id: mainLoader anchors.fill: parent source: "Page.qml" } } qasync-0.28.0/examples/qml_httpx/qml/Page.qml000066400000000000000000000026621505373037500211330ustar00rootroot00000000000000import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Controls.Material 2.15 import QtQuick.Layouts 1.15 Item { ExampleService { id: service // handle value changes inside the service object onValueChanged: { // use value } } Connections { target: service // handle value changes with an external Connection function onValueChanged(value) { // use value } } ColumnLayout { anchors { fill: parent margins: 10 } RowLayout { Layout.fillWidth: true Button { id: button Layout.preferredWidth: 100 enabled: !service.isLoading text: { return service.isLoading ? qsTr("Loading...") : qsTr("Fetch") } onClicked: function() { service.fetch(url.text) } } TextField { id: url Layout.fillWidth: true enabled: !service.isLoading text: qsTr("https://jsonplaceholder.typicode.com/todos/1") } } TextEdit { id: text Layout.fillHeight: true Layout.fillWidth: true // react to value changes from other widgets text: service.value } } } qasync-0.28.0/examples/qml_httpx/service.py000066400000000000000000000022371505373037500207630ustar00rootroot00000000000000import httpx from PySide6.QtCore import QObject, Signal, Property, Slot from qasync import asyncSlot class ExampleService(QObject): valueChanged = Signal(str, arguments=["value"]) loadingChanged = Signal(bool, arguments=["loading"]) def __init__(self, parent=None): QObject.__init__(self, parent) self._value = None self._loading = False def _set_value(self, value): if self._value != value: self._value = value self.valueChanged.emit(value) def _set_loading(self, value): if self._loading != value: self._loading = value self.loadingChanged.emit(value) @Property(str, notify=valueChanged) def value(self) -> str: return self._value @Property(bool, notify=loadingChanged) def isLoading(self) -> bool: return self._loading @asyncSlot(str) async def fetch(self, endpoint: str): if not endpoint: return self._set_loading(True) async with httpx.AsyncClient() as client: resp = await client.get(endpoint) self._set_value(resp.text) self._set_loading(False) qasync-0.28.0/pyproject.toml000066400000000000000000000037451505373037500160340ustar00rootroot00000000000000[build-system] build-backend = "uv_build" requires = ["uv_build>=0.8.3,<0.9.0"] [project] name = "qasync" description = "Python library for using asyncio in Qt-based applications" license = "BSD-2-Clause" license-files = ["LICENSE"] authors = [ { name = "Arve Knudsen", email = "arve.knudsen@gmail.com" }, { name = "Gerard Marull-Paretas", email = "gerard@teslabs.com" }, { name = "Mark Harviston", email = "mark.harviston@gmail.com" }, { name = "Alex March", email = "alexmarch@fastmail.com" }, { name = "Sam McCormack" }, ] maintainers = [{ name = "Alex March", email = "alexmach@fastmail.com" }] readme = "README.md" requires-python = ">=3.8" keywords = ["Qt", "asyncio"] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: X11 Applications :: Qt", "Intended Audience :: Developers", "Operating System :: MacOS", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Topic :: Software Development :: Libraries :: Python Modules", ] version = "0.28.0" dependencies = [] [project.optional-dependencies] typing = ["mypy>=1.0"] [dependency-groups] dev = [ "pytest>=8.4; python_version>='3.9'", "coverage>=7.10; python_version>='3.9'", "pytest==7.4; python_version<'3.9'", "coverage==7.6.1; python_version<'3.9'", ] [project.urls] Homepage = "https://github.com/CabbageDevelopment/qasync" Repository = "https://github.com/CabbageDevelopment/qasync" Releases = "https://github.com/CabbageDevelopment/qasync/releases" Tracker = "https://github.com/CabbageDevelopment/qasync/issues" [tool.ruff] target-version = "py313" [tool.pyright] include = ["src", "tests", "examples"] venvPath = "." venv = ".venv" [tool.pytest.ini_options] markers = ["raises"] testpaths = ["tests"] [tool.coverage.run] relative_files = true command_line = "--module pytest --verbose tests/" source = ["src", "tests"] omit = ["tests/conftest.py"] qasync-0.28.0/src/000077500000000000000000000000001505373037500136765ustar00rootroot00000000000000qasync-0.28.0/src/qasync/000077500000000000000000000000001505373037500151745ustar00rootroot00000000000000qasync-0.28.0/src/qasync/__init__.py000066400000000000000000000725741505373037500173240ustar00rootroot00000000000000""" Implementation of the PEP 3156 Event-Loop with Qt. Copyright (c) 2018 Gerard Marull-Paretas Copyright (c) 2014 Mark Harviston Copyright (c) 2014 Arve Knudsen BSD License """ __all__ = ["QEventLoop", "QThreadExecutor", "asyncSlot", "asyncClose", "asyncWrap"] import asyncio import contextlib import functools import importlib import inspect import itertools import logging import os import sys import time from concurrent.futures import Future from queue import Queue logger = logging.getLogger(__name__) QtModule = None # If QT_API env variable is given, use that or fail trying qtapi_env = os.getenv("QT_API", "").strip().lower() if qtapi_env: env_to_mod_map = { "pyqt5": "PyQt5", "pyqt6": "PyQt6", "pyqt": "PyQt6", "pyside6": "PySide6", "pyside2": "PySide2", "pyside": "PySide6", } if qtapi_env in env_to_mod_map: QtModuleName = env_to_mod_map[qtapi_env] else: raise ImportError( "QT_API environment variable set ({}) but not one of [{}].".format( qtapi_env, ", ".join(env_to_mod_map.keys()) ) ) logger.info("Forcing use of {} as Qt Implementation".format(QtModuleName)) QtModule = importlib.import_module(QtModuleName) # If a Qt lib is already imported, use that if not QtModule: for QtModuleName in ("PyQt5", "PyQt6", "PySide2", "PySide6"): if QtModuleName in sys.modules: QtModule = sys.modules[QtModuleName] break # Try importing qt libs if not QtModule: for QtModuleName in ("PyQt5", "PyQt6", "PySide2", "PySide6"): try: QtModule = importlib.import_module(QtModuleName) except ImportError: continue else: break if not QtModule: raise ImportError("No Qt implementations found") QtCore = importlib.import_module(QtModuleName + ".QtCore", package=QtModuleName) QtGui = importlib.import_module(QtModuleName + ".QtGui", package=QtModuleName) if QtModuleName == "PyQt5": from PyQt5 import QtWidgets from PyQt5.QtCore import pyqtSlot as Slot QApplication = QtWidgets.QApplication AllEvents = QtCore.QEventLoop.ProcessEventsFlags(0x00) elif QtModuleName == "PyQt6": from PyQt6 import QtWidgets from PyQt6.QtCore import pyqtSlot as Slot QApplication = QtWidgets.QApplication AllEvents = QtCore.QEventLoop.ProcessEventsFlag(0x00) elif QtModuleName == "PySide2": from PySide2 import QtWidgets from PySide2.QtCore import Slot QApplication = QtWidgets.QApplication AllEvents = QtCore.QEventLoop.ProcessEventsFlags(0x00) elif QtModuleName == "PySide6": from PySide6 import QtWidgets from PySide6.QtCore import Slot QApplication = QtWidgets.QApplication AllEvents = QtCore.QEventLoop.ProcessEventsFlags(0x00) from ._common import with_logger # noqa @with_logger class _QThreadWorker(QtCore.QThread): """ Read jobs from the queue and then execute them. For use by the QThreadExecutor """ def __init__(self, queue, num, stackSize=None): self.__queue = queue self.__stop = False self.__num = num super().__init__() if stackSize is not None: self.setStackSize(stackSize) def run(self): queue = self.__queue while True: command = queue.get() if command is None: # Stopping... break future, callback, args, kwargs = command self._logger.debug( "#%s got callback %s with args %s and kwargs %s from queue", self.__num, callback, args, kwargs, ) if future.set_running_or_notify_cancel(): self._logger.debug("Invoking callback") try: r = callback(*args, **kwargs) except Exception as err: self._logger.debug("Setting Future exception: %s", err) future.set_exception(err) else: self._logger.debug("Setting Future result: %s", r) future.set_result(r) finally: # Release potential reference r = None # noqa else: self._logger.debug("Future was canceled") # Delete references del command, future, callback, args, kwargs self._logger.debug("Thread #%s stopped", self.__num) def wait(self): self._logger.debug("Waiting for thread #%s to stop...", self.__num) super().wait() @with_logger class QThreadExecutor: """ ThreadExecutor that produces QThreads. Same API as `concurrent.futures.Executor` >>> from qasync import QThreadExecutor >>> with QThreadExecutor(5) as executor: ... f = executor.submit(lambda x: 2 + x, 2) ... r = f.result() ... assert r == 4 """ def __init__(self, max_workers=10, stack_size=None): super().__init__() self.__max_workers = max_workers self.__queue = Queue() if stack_size is None: # Match cpython/Python/thread_pthread.h if sys.platform.startswith("darwin"): stack_size = 16 * 2**20 elif sys.platform.startswith("freebsd"): stack_size = 4 * 2**20 elif sys.platform.startswith("aix"): stack_size = 2 * 2**20 self.__workers = [ _QThreadWorker(self.__queue, i + 1, stack_size) for i in range(max_workers) ] self.__been_shutdown = False for w in self.__workers: w.start() def submit(self, callback, *args, **kwargs): if self.__been_shutdown: raise RuntimeError("QThreadExecutor has been shutdown") future = Future() self._logger.debug( "Submitting callback %s with args %s and kwargs %s to thread worker queue", callback, args, kwargs, ) self.__queue.put((future, callback, args, kwargs)) return future def map(self, func, *iterables, timeout=None): raise NotImplementedError("use as_completed on the event loop") def shutdown(self, wait=True): if self.__been_shutdown: raise RuntimeError("QThreadExecutor has been shutdown") self.__been_shutdown = True self._logger.debug("Shutting down") for i in range(len(self.__workers)): # Signal workers to stop self.__queue.put(None) if wait: for w in self.__workers: w.wait() def __enter__(self, *args): if self.__been_shutdown: raise RuntimeError("QThreadExecutor has been shutdown") return self def __exit__(self, *args): self.shutdown() def _format_handle(handle: asyncio.Handle): cb = getattr(handle, "_callback", None) if isinstance(getattr(cb, "__self__", None), asyncio.tasks.Task): return repr(cb.__self__) return str(handle) def _make_signaller(qtimpl_qtcore, *args): class Signaller(qtimpl_qtcore.QObject): try: signal = qtimpl_qtcore.Signal(*args) except AttributeError: signal = qtimpl_qtcore.pyqtSignal(*args) return Signaller() @with_logger class _SimpleTimer(QtCore.QObject): def __init__(self): super().__init__() self.__callbacks = {} self._stopped = False self.__debug_enabled = False def add_callback(self, handle, delay=0): timerid = self.startTimer(int(max(0, delay) * 1000)) self.__log_debug("Registering timer id %s", timerid) assert timerid not in self.__callbacks self.__callbacks[timerid] = handle return handle def timerEvent(self, event): # noqa: N802 timerid = event.timerId() self.__log_debug("Timer event on id %s", timerid) if self._stopped: self.__log_debug("Timer stopped, killing %s", timerid) self.killTimer(timerid) del self.__callbacks[timerid] else: try: handle = self.__callbacks[timerid] except KeyError as e: self.__log_debug(e) pass else: if handle._cancelled: self.__log_debug("Handle %s cancelled", handle) else: if self.__debug_enabled: # This may not be the most efficient thing to do, but it removes the need to sync # "slow_callback_duration" and "_current_handle" variables loop = asyncio.get_event_loop() try: loop._current_handle = handle self._logger.debug("Calling handle %s", handle) t0 = time.time() handle._run() dt = time.time() - t0 if dt >= loop.slow_callback_duration: self._logger.warning( "Executing %s took %.3f seconds", _format_handle(handle), dt, ) finally: loop._current_handle = None else: handle._run() finally: del self.__callbacks[timerid] handle = None self.killTimer(timerid) def stop(self): self.__log_debug("Stopping timers") self._stopped = True def set_debug(self, enabled): self.__debug_enabled = enabled def __log_debug(self, *args, **kwargs): if self.__debug_enabled: self._logger.debug(*args, **kwargs) def _fileno(fd): if isinstance(fd, int): return fd try: return int(fd.fileno()) except (AttributeError, TypeError, ValueError): raise ValueError(f"Invalid file object: {fd!r}") from None @with_logger class _QEventLoop: """ Implementation of asyncio event loop that uses the Qt Event loop. >>> import asyncio >>> >>> app = getfixture('application') >>> >>> async def xplusy(x, y): ... await asyncio.sleep(.1) ... assert x + y == 4 ... await asyncio.sleep(.1) >>> >>> asyncio.run(xplusy(2, 2), loop_factory=lambda:QEventLoop(app)) If the event loop shall be used with an existing and already running QApplication it must be specified in the constructor via already_running=True In this case the user is responsible for loop cleanup with stop() and close() The set_running_loop parameter is there for backwards compatibility and does nothing. """ def __init__(self, app=None, set_running_loop=False, already_running=False): self.__app = app or QApplication.instance() assert self.__app is not None, "No QApplication has been instantiated" self.__is_running = False self.__debug_enabled = False self.__default_executor = None self.__exception_handler = None self._read_notifiers = {} self._write_notifiers = {} self._timer = _SimpleTimer() self.__call_soon_signaller = signaller = _make_signaller(QtCore, object, tuple) self.__call_soon_signal = signaller.signal self.__call_soon_signal.connect( lambda callback, args: self.call_soon(callback, *args) ) assert self.__app is not None super().__init__() # We have to set __is_running to True after calling # super().__init__() because of a bug in BaseEventLoop. if already_running: self.__is_running = True # it must be ensured that all pre- and # postprocessing for the eventloop is done self._before_run_forever() self.__app.aboutToQuit.connect(self._after_run_forever) # for asyncio to recognize the already running loop asyncio.events._set_running_loop(self) def run_forever(self): """Run eventloop forever.""" if self.__is_running: raise RuntimeError("Event loop already running") self.__is_running = True self._before_run_forever() try: self.__log_debug("Starting Qt event loop") asyncio.events._set_running_loop(self) rslt = -1 if hasattr(self.__app, "exec"): rslt = self.__app.exec() else: rslt = self.__app.exec_() self.__log_debug("Qt event loop ended with result %s", rslt) return rslt finally: asyncio.events._set_running_loop(None) self._after_run_forever() self.__is_running = False def run_until_complete(self, future): """Run until Future is complete.""" if self.__is_running: raise RuntimeError("Event loop already running") self.__log_debug("Running %s until complete", future) future = asyncio.ensure_future(future, loop=self) def stop(*args): self.stop() # noqa future.add_done_callback(stop) try: self.run_forever() finally: future.remove_done_callback(stop) self.__app.eventDispatcher().processEvents( AllEvents ) # run loop one last time to process all the events if not future.done(): raise RuntimeError("Event loop stopped before Future completed.") self.__log_debug("Future %s finished running", future) return future.result() def stop(self): """Stop event loop.""" if not self.__is_running: self.__log_debug("Already stopped") return self.__log_debug("Stopping event loop...") self.__is_running = False self.__app.exit() self.__log_debug("Stopped event loop") def is_running(self): """Return True if the event loop is running, False otherwise.""" return self.__is_running def close(self): """ Release all resources used by the event loop. The loop cannot be restarted after it has been closed. """ if self.is_running(): raise RuntimeError("Cannot close a running event loop") if self.is_closed(): return self.__log_debug("Closing event loop...") if self.__default_executor is not None: self.__default_executor.shutdown() if self.__call_soon_signal: self.__call_soon_signal.disconnect() super().close() self._timer.stop() self.__app = None for notifier in itertools.chain( self._read_notifiers.values(), self._write_notifiers.values() ): notifier.setEnabled(False) notifier.activated["int"].disconnect() self._read_notifiers = None self._write_notifiers = None def call_later(self, delay, callback, *args, context=None): """Register callback to be invoked after a certain delay.""" if asyncio.iscoroutinefunction(callback): raise TypeError("coroutines cannot be used with call_later") if not callable(callback): raise TypeError( "callback must be callable: {}".format(type(callback).__name__) ) self.__log_debug( "Registering callback %s to be invoked with arguments %s after %s second(s)", callback, args, delay, ) if sys.version_info >= (3, 7): return self._add_callback( asyncio.Handle(callback, args, self, context=context), delay ) return self._add_callback(asyncio.Handle(callback, args, self), delay) def _add_callback(self, handle, delay=0): return self._timer.add_callback(handle, delay) def call_soon(self, callback, *args, context=None): """Register a callback to be run on the next iteration of the event loop.""" return self.call_later(0, callback, *args, context=context) def call_at(self, when, callback, *args, context=None): """Register callback to be invoked at a certain time.""" return self.call_later(when - self.time(), callback, *args, context=context) def time(self): """Get time according to event loop's clock.""" return time.monotonic() def _add_reader(self, fd, callback, *args): """Register a callback for when a file descriptor is ready for reading.""" self._check_closed() try: existing = self._read_notifiers[fd] except KeyError: pass else: # this is necessary to avoid race condition-like issues existing.setEnabled(False) existing.activated["int"].disconnect() # will get overwritten by the assignment below anyways notifier = QtCore.QSocketNotifier(_fileno(fd), QtCore.QSocketNotifier.Type.Read) notifier.setEnabled(True) self.__log_debug("Adding reader callback for file descriptor %s", fd) notifier.activated["int"].connect( lambda: self.__on_notifier_ready( self._read_notifiers, notifier, fd, callback, args ) # noqa: C812 ) self._read_notifiers[fd] = notifier def _remove_reader(self, fd): """Remove reader callback.""" if self.is_closed(): return self.__log_debug("Removing reader callback for file descriptor %s", fd) try: notifier = self._read_notifiers.pop(fd) except KeyError: return False else: notifier.setEnabled(False) notifier.activated["int"].disconnect() return True def _add_writer(self, fd, callback, *args): """Register a callback for when a file descriptor is ready for writing.""" self._check_closed() try: existing = self._write_notifiers[fd] except KeyError: pass else: # this is necessary to avoid race condition-like issues existing.setEnabled(False) existing.activated["int"].disconnect() # will get overwritten by the assignment below anyways notifier = QtCore.QSocketNotifier( _fileno(fd), QtCore.QSocketNotifier.Type.Write, ) notifier.setEnabled(True) self.__log_debug("Adding writer callback for file descriptor %s", fd) notifier.activated["int"].connect( lambda: self.__on_notifier_ready( self._write_notifiers, notifier, fd, callback, args ) # noqa: C812 ) self._write_notifiers[fd] = notifier def _remove_writer(self, fd): """Remove writer callback.""" if self.is_closed(): return self.__log_debug("Removing writer callback for file descriptor %s", fd) try: notifier = self._write_notifiers.pop(fd) except KeyError: return False else: notifier.setEnabled(False) notifier.activated["int"].disconnect() return True def __notifier_cb_wrapper(self, notifiers, notifier, fd, callback, args): # This wrapper gets called with a certain delay. We cannot know # for sure that the notifier is still the current notifier for # the fd. if notifiers.get(fd, None) is not notifier: return try: callback(*args) finally: # The notifier might have been overriden by the # callback. We must not re-enable it in that case. if notifiers.get(fd, None) is notifier: notifier.setEnabled(True) def __on_notifier_ready(self, notifiers, notifier, fd, callback, args): if fd not in notifiers: self._logger.warning( "Socket notifier for fd %s is ready, even though it should " "be disabled, not calling %s and disabling", fd, callback, ) notifier.setEnabled(False) notifier.activated["int"].disconnect() return # It can be necessary to disable QSocketNotifier when e.g. checking # ZeroMQ sockets for events assert notifier.isEnabled() self.__log_debug("Socket notifier for fd %s is ready", fd) notifier.setEnabled(False) self.call_soon( self.__notifier_cb_wrapper, notifiers, notifier, fd, callback, args ) # Methods for interacting with threads. def call_soon_threadsafe(self, callback, *args, context=None): """Thread-safe version of call_soon.""" self.__call_soon_signal.emit(callback, args) def run_in_executor(self, executor, callback, *args): """Run callback in executor. If no executor is provided, the default executor will be used, which defers execution to a background thread. """ self.__log_debug("Running callback %s with args %s in executor", callback, args) if isinstance(callback, asyncio.Handle): assert not args assert not isinstance(callback, asyncio.TimerHandle) if callback._cancelled: f = asyncio.Future() f.set_result(None) return f callback, args = callback.callback, callback.args if executor is None: self.__log_debug("Using default executor") executor = self.__default_executor if executor is None: self.__log_debug("Creating default executor") executor = self.__default_executor = QThreadExecutor() return asyncio.wrap_future(executor.submit(callback, *args)) def set_default_executor(self, executor): self.__default_executor = executor # Error handlers. def set_exception_handler(self, handler): self.__exception_handler = handler def default_exception_handler(self, context): """Handle exceptions. This is the default exception handler. This is called when an exception occurs and no exception handler is set, and can be called by a custom exception handler that wants to defer to the default behavior. context parameter has the same meaning as in `call_exception_handler()`. """ self.__log_debug("Default exception handler executing") message = context.get("message") if not message: message = "Unhandled exception in event loop" try: exception = context["exception"] except KeyError: exc_info = False else: exc_info = (type(exception), exception, exception.__traceback__) log_lines = [message] for key in [k for k in sorted(context) if k not in {"message", "exception"}]: log_lines.append("{}: {!r}".format(key, context[key])) self.__log_error("\n".join(log_lines), exc_info=exc_info) def call_exception_handler(self, context): if self.__exception_handler is None: try: self.default_exception_handler(context) except Exception: # Second protection layer for unexpected errors # in the default implementation, as well as for subclassed # event loops with overloaded "default_exception_handler". self.__log_error( "Exception in default exception handler", exc_info=True ) return try: self.__exception_handler(self, context) except Exception as exc: # Exception in the user set custom exception handler. try: # Let's try the default handler. self.default_exception_handler( { "message": "Unhandled error in custom exception handler", "exception": exc, "context": context, } ) except Exception: # Guard 'default_exception_handler' in case it's # overloaded. self.__log_error( "Exception in default exception handler while handling an unexpected error " "in custom exception handler", exc_info=True, ) # Debug flag management. def get_debug(self): return self.__debug_enabled def set_debug(self, enabled): super().set_debug(enabled) self.__debug_enabled = enabled self._timer.set_debug(enabled) def __enter__(self): return self def __exit__(self, *args): self.stop() self.close() def __log_debug(self, *args, **kwargs): if self.__debug_enabled: self._logger.debug(*args, **kwargs) @classmethod def __log_error(cls, *args, **kwds): # In some cases, the error method itself fails, don't have a lot of options in that case try: cls._logger.error(*args, **kwds) except: # noqa E722 sys.stderr.write("{!r}, {!r}\n".format(args, kwds)) from ._unix import _SelectorEventLoop # noqa QSelectorEventLoop = type("QSelectorEventLoop", (_QEventLoop, _SelectorEventLoop), {}) if os.name == "nt": from ._windows import _ProactorEventLoop QIOCPEventLoop = type("QIOCPEventLoop", (_QEventLoop, _ProactorEventLoop), {}) QEventLoop = QIOCPEventLoop else: QEventLoop = QSelectorEventLoop class _Cancellable: def __init__(self, timer, loop): self.__timer = timer self.__loop = loop def cancel(self): self.__timer.stop() def asyncClose(fn): """Allow to run async code before application is closed.""" @functools.wraps(fn) def wrapper(*args, **kwargs): f = asyncio.ensure_future(fn(*args, **kwargs)) while not f.done(): QApplication.instance().processEvents() return wrapper def asyncSlot(*args, **kwargs): """Make a Qt async slot run on asyncio loop.""" def _error_handler(task): try: task.result() except Exception: sys.excepthook(*sys.exc_info()) except asyncio.CancelledError: pass def outer_decorator(fn): @Slot(*args, **kwargs) @functools.wraps(fn) def wrapper(*args, **kwargs): # Qt ignores trailing args from a signal but python does # not so inspect the slot signature and if it's not # callable try removing args until it is. task = None while len(args): try: inspect.signature(fn).bind(*args, **kwargs) except TypeError: if len(args): # Only convert args to a list if we need to pop() args = list(args) args.pop() continue else: task = asyncio.ensure_future(fn(*args, **kwargs)) task.add_done_callback(_error_handler) break if task is None: raise TypeError( "asyncSlot was not callable from Signal. Potential signature mismatch." ) return task return wrapper return outer_decorator async def asyncWrap(fn, *args, **kwargs): """ Wrap a blocking function as an asynchronous and run it on the native Qt event loop. The function will be scheduled using a one shot QTimer which prevents blocking the QEventLoop. An example usage of this is raising a modal dialogue inside an asyncSlot. ```python async def before_shutdown(self): await asyncio.sleep(2) @asyncSlot() async def shutdown_clicked(self): # do some work async asyncio.create_task(self.before_shutdown()) # run on the native Qt loop, not blocking the QEventLoop result = await asyncWrap( lambda: QMessageBox.information(None, "Done", "It is now safe to shutdown.") ) if result == QMessageBox.StandardButton.Ok: app.exit(0) ``` """ future = asyncio.Future() @functools.wraps(fn) def helper(): try: result = fn(*args, **kwargs) except Exception as e: future.set_exception(e) else: future.set_result(result) # Schedule the helper to run in the next event loop iteration QtCore.QTimer.singleShot(0, helper) return await future def _get_qevent_loop(): return QEventLoop(QApplication.instance() or QApplication(sys.argv)) if sys.version_info >= (3, 12): def run(*args, **kwargs): return asyncio.run( *args, **kwargs, loop_factory=_get_qevent_loop, ) else: # backwards compatibility with event loop policies class DefaultQEventLoopPolicy(asyncio.DefaultEventLoopPolicy): def new_event_loop(self): return _get_qevent_loop() @contextlib.contextmanager def _set_event_loop_policy(policy): old_policy = asyncio.get_event_loop_policy() asyncio.set_event_loop_policy(policy) try: yield finally: asyncio.set_event_loop_policy(old_policy) def run(*args, **kwargs): with _set_event_loop_policy(DefaultQEventLoopPolicy()): return asyncio.run(*args, **kwargs) qasync-0.28.0/src/qasync/_common.py000066400000000000000000000011671505373037500172020ustar00rootroot00000000000000""" Mostly irrelevant, but useful utilities common to UNIX and Windows. Copyright (c) 2018 Gerard Marull-Paretas Copyright (c) 2014 Mark Harviston Copyright (c) 2014 Arve Knudsen BSD License """ import logging def with_logger(cls): """Class decorator to add a logger to a class.""" attr_name = "_logger" cls_name = cls.__qualname__ module = cls.__module__ if module is not None: cls_name = module + "." + cls_name else: raise AssertionError setattr(cls, attr_name, logging.getLogger(cls_name)) return cls qasync-0.28.0/src/qasync/_unix.py000066400000000000000000000142511505373037500166730ustar00rootroot00000000000000""" UNIX specific qasync functionality. Copyright (c) 2018 Gerard Marull-Paretas Copyright (c) 2014 Mark Harviston Copyright (c) 2014 Arve Knudsen BSD License """ import asyncio import collections import selectors from . import QtCore, _fileno, with_logger EVENT_READ = 1 << 0 EVENT_WRITE = 1 << 1 class _SelectorMapping(collections.abc.Mapping): """Mapping of file objects to selector keys.""" def __init__(self, selector): self._selector = selector def __len__(self): return len(self._selector._fd_to_key) def __getitem__(self, fileobj): try: fd = self._selector._fileobj_lookup(fileobj) return self._selector._fd_to_key[fd] except KeyError: raise KeyError("{!r} is not registered".format(fileobj)) from None def __iter__(self): return iter(self._selector._fd_to_key) @with_logger class _Selector(selectors.BaseSelector): def __init__(self, parent): # this maps file descriptors to keys self._fd_to_key = {} # read-only mapping returned by get_map() self.__map = _SelectorMapping(self) self.__read_notifiers = {} self.__write_notifiers = {} self.__parent = parent def select(self, *args, **kwargs): """Implement abstract method even though we don't need it.""" raise NotImplementedError def _fileobj_lookup(self, fileobj): """Return a file descriptor from a file object. This wraps _fileno() to do an exhaustive search in case the object is invalid but we still have it in our map. This is used by unregister() so we can unregister an object that was previously registered even if it is closed. It is also used by _SelectorMapping. """ try: return _fileno(fileobj) except ValueError: # Do an exhaustive search. for key in self._fd_to_key.values(): if key.fileobj is fileobj: return key.fd # Raise ValueError after all. raise def register(self, fileobj, events, data=None): if (not events) or (events & ~(EVENT_READ | EVENT_WRITE)): raise ValueError("Invalid events: {!r}".format(events)) key = selectors.SelectorKey( fileobj, self._fileobj_lookup(fileobj), events, data ) if key.fd in self._fd_to_key: raise KeyError("{!r} (FD {}) is already registered".format(fileobj, key.fd)) self._fd_to_key[key.fd] = key if events & EVENT_READ: notifier = QtCore.QSocketNotifier(key.fd, QtCore.QSocketNotifier.Read) notifier.activated["int"].connect(self.__on_read_activated) self.__read_notifiers[key.fd] = notifier if events & EVENT_WRITE: notifier = QtCore.QSocketNotifier(key.fd, QtCore.QSocketNotifier.Write) notifier.activated["int"].connect(self.__on_write_activated) self.__write_notifiers[key.fd] = notifier return key def __on_read_activated(self, fd): self._logger.debug("File %s ready to read", fd) key = self._key_from_fd(fd) if key: self.__parent._process_event(key, EVENT_READ & key.events) def __on_write_activated(self, fd): self._logger.debug("File %s ready to write", fd) key = self._key_from_fd(fd) if key: self.__parent._process_event(key, EVENT_WRITE & key.events) def unregister(self, fileobj): def drop_notifier(notifiers): try: notifier = notifiers.pop(key.fd) except KeyError: pass else: notifier.activated["int"].disconnect() try: key = self._fd_to_key.pop(self._fileobj_lookup(fileobj)) except KeyError: raise KeyError("{!r} is not registered".format(fileobj)) from None drop_notifier(self.__read_notifiers) drop_notifier(self.__write_notifiers) return key def modify(self, fileobj, events, data=None): try: key = self._fd_to_key[self._fileobj_lookup(fileobj)] except KeyError: raise KeyError("{!r} is not registered".format(fileobj)) from None if events != key.events: self.unregister(fileobj) key = self.register(fileobj, events, data) elif data != key.data: # Use a shortcut to update the data. key = key._replace(data=data) self._fd_to_key[key.fd] = key return key def close(self): self._logger.debug("Closing") self._fd_to_key.clear() self.__read_notifiers.clear() self.__write_notifiers.clear() def get_map(self): return self.__map def _key_from_fd(self, fd): """ Return the key associated to a given file descriptor. Parameters: fd -- file descriptor Returns: corresponding key, or None if not found """ try: return self._fd_to_key[fd] except KeyError: return None class _SelectorEventLoop(asyncio.SelectorEventLoop): def __init__(self): self._signal_safe_callbacks = [] selector = _Selector(self) asyncio.SelectorEventLoop.__init__(self, selector) def _before_run_forever(self): pass def _after_run_forever(self): pass def _process_event(self, key, mask): """Selector has delivered us an event.""" self._logger.debug("Processing event with key %s and mask %s", key, mask) fileobj, (reader, writer) = key.fileobj, key.data if mask & selectors.EVENT_READ and reader is not None: if reader._cancelled: self.remove_reader(fileobj) else: self._logger.debug("Invoking reader callback: %s", reader) reader._run() if mask & selectors.EVENT_WRITE and writer is not None: if writer._cancelled: self.remove_writer(fileobj) else: self._logger.debug("Invoking writer callback: %s", writer) writer._run() qasync-0.28.0/src/qasync/_windows.py000066400000000000000000000156611505373037500174100ustar00rootroot00000000000000""" Windows specific qasync functionality. Copyright (c) 2018 Gerard Marull-Paretas Copyright (c) 2014 Mark Harviston Copyright (c) 2014 Arve Knudsen BSD License """ import asyncio import sys import threading try: import _overlapped import _winapi from asyncio import windows_events except ImportError: # noqa pass # w/o guarding this import py.test can't gather doctests on platforms w/o _winapi import math from . import QtCore, _make_signaller from ._common import with_logger UINT32_MAX = 0xFFFFFFFF class _ProactorEventLoop(asyncio.ProactorEventLoop): """Proactor based event loop.""" def __init__(self): super().__init__(_IocpProactor()) self.__event_signaller = _make_signaller(QtCore, list) self.__event_signal = self.__event_signaller.signal self.__event_signal.connect(self._process_events) self.__event_poller = _EventPoller(self.__event_signal) def _process_events(self, events): """Process events from proactor.""" for f, callback, transferred, key, ov in events: try: self._logger.debug("Invoking event callback %s", callback) value = callback(transferred, key, ov) except OSError as e: self._logger.debug("Event callback failed", exc_info=sys.exc_info()) if not f.done(): f.set_exception(e) else: if not f.cancelled(): f.set_result(value) def _before_run_forever(self): self.__event_poller.start(self._proactor) def _after_run_forever(self): self.__event_poller.stop() @with_logger class _IocpProactor(windows_events.IocpProactor): def __init__(self): self.__events = [] super(_IocpProactor, self).__init__() self._lock = threading.Lock() def select(self, timeout=None): """Override in order to handle events in a threadsafe manner.""" if not self.__events: self._poll(timeout) tmp = self.__events self.__events = [] return tmp def close(self): self._logger.debug("Closing") super(_IocpProactor, self).close() # Wrap all I/O submission methods to acquire the internal lock first; listed # in the order they appear in the base class source code. def recv(self, conn, nbytes, flags=0): with self._lock: return super(_IocpProactor, self).recv(conn, nbytes, flags) def recv_into(self, conn, buf, flags=0): with self._lock: return super(_IocpProactor, self).recv_into(conn, buf, flags) def recvfrom(self, conn, nbytes, flags=0): with self._lock: return super(_IocpProactor, self).recvfrom(conn, nbytes, flags) def recvfrom_into(self, conn, buf, flags=0): with self._lock: return super(_IocpProactor, self).recvfrom_into(conn, buf, flags) def sendto(self, conn, buf, flags=0, addr=None): with self._lock: return super(_IocpProactor, self).sendto(conn, buf, flags, addr) def send(self, conn, buf, flags=0): with self._lock: return super(_IocpProactor, self).send(conn, buf, flags) def accept(self, listener): with self._lock: return super(_IocpProactor, self).accept(listener) def connect(self, conn, address): with self._lock: return super(_IocpProactor, self).connect(conn, address) def sendfile(self, sock, file, offset, count): with self._lock: return super(_IocpProactor, self).sendfile(sock, file, offset, count) def accept_pipe(self, pipe): with self._lock: return super(_IocpProactor, self).accept_pipe(pipe) # connect_pipe() does not actually use the delayed completion machinery. # This takes care of wait_for_handle() too. def _wait_for_handle(self, handle, timeout, _is_cancel): with self._lock: return super(_IocpProactor, self)._wait_for_handle( handle, timeout, _is_cancel ) def _poll(self, timeout=None): """Override in order to handle events in a threadsafe manner.""" if timeout is None: ms = UINT32_MAX # wait for eternity elif timeout < 0: raise ValueError("negative timeout") else: # GetQueuedCompletionStatus() has a resolution of 1 millisecond, # round away from zero to wait *at least* timeout seconds. ms = math.ceil(timeout * 1e3) if ms >= UINT32_MAX: raise ValueError("timeout too big") while True: status = _overlapped.GetQueuedCompletionStatus(self._iocp, ms) if status is None: break ms = 0 with self._lock: err, transferred, key, address = status try: f, ov, obj, callback = self._cache.pop(address) except KeyError: # key is either zero, or it is used to return a pipe # handle which should be closed to avoid a leak. if key not in (0, _overlapped.INVALID_HANDLE_VALUE): _winapi.CloseHandle(key) continue if obj in self._stopped_serving: f.cancel() # Futures might already be resolved or cancelled elif not f.done(): self.__events.append((f, callback, transferred, key, ov)) # Remove unregistered futures for ov in self._unregistered: self._cache.pop(ov.address, None) self._unregistered.clear() @with_logger class _EventWorker(QtCore.QThread): def __init__(self, proactor, parent): super().__init__() self.__stop = False self.__proactor = proactor self.__sig_events = parent.sig_events self.__semaphore = QtCore.QSemaphore() def start(self): super().start() self.__semaphore.acquire() def stop(self): self.__stop = True # Wait for thread to end self.wait() def run(self): self._logger.debug("Thread started") self.__semaphore.release() while not self.__stop: events = self.__proactor.select(0.01) if events: self._logger.debug("Got events from poll: %s", events) self.__sig_events.emit(events) self._logger.debug("Exiting thread") @with_logger class _EventPoller: """Polling of events in separate thread.""" def __init__(self, sig_events): self.sig_events = sig_events def start(self, proactor): self._logger.debug("Starting (proactor: %s)...", proactor) self.__worker = _EventWorker(proactor, self) self.__worker.start() def stop(self): self._logger.debug("Stopping worker thread...") self.__worker.stop() qasync-0.28.0/tests/000077500000000000000000000000001505373037500142515ustar00rootroot00000000000000qasync-0.28.0/tests/conftest.py000066400000000000000000000007221505373037500164510ustar00rootroot00000000000000""" Copyright (c) 2018 Gerard Marull-Paretas Copyright (c) 2014 Mark Harviston Copyright (c) 2014 Arve Knudsen BSD License """ import logging from pytest import fixture logging.basicConfig( level=logging.DEBUG, format="%(levelname)s\t%(filename)s:%(lineno)s %(message)s" ) @fixture(scope="session") def application(): from qasync import QApplication return QApplication([]) qasync-0.28.0/tests/test_qeventloop.py000066400000000000000000000642321505373037500200650ustar00rootroot00000000000000# © 2018 Gerard Marull-Paretas # © 2014 Mark Harviston # © 2014 Arve Knudsen # BSD License import asyncio import ctypes import logging import multiprocessing import os import socket import subprocess import sys import threading import time from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor import pytest import qasync @pytest.fixture def loop(request, application): lp = qasync.QEventLoop(application) asyncio.set_event_loop(lp) additional_exceptions = [] def fin(): sys.excepthook = orig_excepthook try: lp.close() finally: asyncio.set_event_loop(None) for exc in additional_exceptions: if ( os.name == "nt" and isinstance(exc["exception"], WindowsError) and exc["exception"].winerror == 6 ): # ignore Invalid Handle Errors continue raise exc["exception"] def except_handler(loop, ctx): additional_exceptions.append(ctx) def excepthook(type, *args): lp.stop() orig_excepthook(type, *args) orig_excepthook = sys.excepthook sys.excepthook = excepthook lp.set_exception_handler(except_handler) request.addfinalizer(fin) return lp @pytest.fixture( params=[None, qasync.QThreadExecutor, ThreadPoolExecutor, ProcessPoolExecutor], ) def executor(request): exc_cls = request.param if exc_cls is None: return None exc = exc_cls(1) # FIXME? fixed number of workers? request.addfinalizer(exc.shutdown) return exc ExceptionTester = type( "ExceptionTester", (Exception,), {} ) # to make flake8 not complain class TestCanRunTasksInExecutor: """ Test Cases Concerning running jobs in Executors. This needs to be a class because pickle can't serialize closures, but can serialize bound methods. multiprocessing can only handle pickleable functions. """ def test_can_run_tasks_in_executor(self, loop, executor): """Verify that tasks can be run in an executor.""" logging.debug("Loop: {!r}".format(loop)) logging.debug("Executor: {!r}".format(executor)) manager = multiprocessing.Manager() was_invoked = manager.Value(ctypes.c_int, 0) logging.debug("running until complete") loop.run_until_complete(self.blocking_task(loop, executor, was_invoked)) logging.debug("ran") assert was_invoked.value == 1 def test_can_handle_exception_in_executor(self, loop, executor): with pytest.raises(ExceptionTester) as excinfo: loop.run_until_complete( asyncio.wait_for( loop.run_in_executor(executor, self.blocking_failure), timeout=10.0, ) ) assert str(excinfo.value) == "Testing" def blocking_failure(self): logging.debug("raising") try: raise ExceptionTester("Testing") finally: logging.debug("raised!") def blocking_func(self, was_invoked): logging.debug("start blocking_func()") was_invoked.value = 1 logging.debug("end blocking_func()") async def blocking_task(self, loop, executor, was_invoked): logging.debug("start blocking task()") fut = loop.run_in_executor(executor, self.blocking_func, was_invoked) await asyncio.wait_for(fut, timeout=10.0) logging.debug("start blocking task()") def test_can_execute_subprocess(loop): """Verify that a subprocess can be executed.""" async def mycoro(): process = await asyncio.create_subprocess_exec( sys.executable or "python", "-c", "import sys; sys.exit(5)" ) await process.wait() assert process.returncode == 5 loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=10.0)) def test_can_read_subprocess(loop): """Verify that a subprocess's data can be read from stdout.""" async def mycoro(): process = await asyncio.create_subprocess_exec( sys.executable or "python", "-c", 'print("Hello async world!")', stdout=subprocess.PIPE, ) if process.stdout is None: raise Exception("Output from the process is none") received_stdout = await process.stdout.readexactly(len(b"Hello async world!\n")) await process.wait() assert process.returncode == 0 assert received_stdout.strip() == b"Hello async world!" loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=10.0)) def test_can_communicate_subprocess(loop): """Verify that a subprocess's data can be passed in/out via stdin/stdout.""" async def mycoro(): process = await asyncio.create_subprocess_exec( sys.executable or "python", "-c", "print(input())", stdout=subprocess.PIPE, stdin=subprocess.PIPE, ) received_stdout, received_stderr = await process.communicate( b"Hello async world!\n" ) await process.wait() assert process.returncode == 0 assert received_stdout.strip() == b"Hello async world!" loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=10.0)) def test_can_terminate_subprocess(loop): """Verify that a subprocess can be terminated.""" # Start a never-ending process async def mycoro(): process = await asyncio.create_subprocess_exec( sys.executable or "python", "-c", "import time\nwhile True: time.sleep(1)" ) process.terminate() await process.wait() assert process.returncode != 0 loop.run_until_complete(mycoro()) @pytest.mark.raises(ExceptionTester) def test_loop_callback_exceptions_bubble_up(loop): """Verify that test exceptions raised in event loop callbacks bubble up.""" def raise_test_exception(): raise ExceptionTester("Test Message") loop.call_soon(raise_test_exception) loop.run_until_complete(asyncio.sleep(0.1)) def test_loop_running(loop): """Verify that loop.is_running returns True when running.""" async def is_running(): nonlocal loop assert loop.is_running() loop.run_until_complete(is_running()) def test_loop_not_running(loop): """Verify that loop.is_running returns False when not running.""" assert not loop.is_running() def test_get_running_loop_fails_after_completion(loop): """Verify that after loop stops, asyncio._get_running_loop() correctly returns None.""" async def is_running_loop(): nonlocal loop assert asyncio._get_running_loop() == loop loop.run_until_complete(is_running_loop()) assert asyncio._get_running_loop() is None def test_loop_can_run_twice(loop): """Verify that loop is correctly reset as asyncio._get_running_loop() when restarted.""" async def is_running_loop(): nonlocal loop assert asyncio._get_running_loop() == loop loop.run_until_complete(is_running_loop()) loop.run_until_complete(is_running_loop()) def test_can_function_as_context_manager(application): """Verify that a QEventLoop can function as its own context manager.""" with qasync.QEventLoop(application) as loop: assert isinstance(loop, qasync.QEventLoop) loop.call_soon(loop.stop) loop.run_forever() def test_future_not_done_on_loop_shutdown(loop): """Verify RuntimError occurs when loop stopped before Future completed with run_until_complete.""" loop.call_later(0.1, loop.stop) fut = asyncio.Future() with pytest.raises(RuntimeError): loop.run_until_complete(fut) def test_call_later_must_not_coroutine(loop): """Verify TypeError occurs call_later is given a coroutine.""" async def mycoro(): pass with pytest.raises(TypeError): loop.call_soon(mycoro) def test_call_later_must_be_callable(loop): """Verify TypeError occurs call_later is not given a callable.""" not_callable = object() with pytest.raises(TypeError): loop.call_soon(not_callable) def test_call_at(loop): """Verify that loop.call_at works as expected.""" def mycallback(): nonlocal was_invoked was_invoked = True was_invoked = False loop.call_at(loop.time() + 0.05, mycallback) loop.run_until_complete(asyncio.sleep(0.1)) assert was_invoked def test_get_set_debug(loop): """Verify get_debug and set_debug work as expected.""" loop.set_debug(True) assert loop.get_debug() loop.set_debug(False) assert not loop.get_debug() @pytest.fixture def sock_pair(request): """Create socket pair. If socket.socketpair isn't available, we emulate it. """ def fin(): if client_sock is not None: client_sock.close() if srv_sock is not None: srv_sock.close() client_sock = srv_sock = None request.addfinalizer(fin) # See if socketpair() is available. have_socketpair = hasattr(socket, "socketpair") if have_socketpair: client_sock, srv_sock = socket.socketpair() return client_sock, srv_sock # Create a non-blocking temporary server socket temp_srv_sock = socket.socket() temp_srv_sock.setblocking(False) temp_srv_sock.bind(("", 0)) port = temp_srv_sock.getsockname()[1] temp_srv_sock.listen(1) # Create non-blocking client socket client_sock = socket.socket() client_sock.setblocking(False) try: client_sock.connect(("localhost", port)) except socket.error as err: # Error 10035 (operation would block) is not an error, as we're doing this with a # non-blocking socket. if err.errno != 10035: raise # Use select to wait for connect() to succeed. import select timeout = 1 readable = select.select([temp_srv_sock], [], [], timeout)[0] if temp_srv_sock not in readable: raise Exception("Client socket not connected in {} second(s)".format(timeout)) srv_sock, _ = temp_srv_sock.accept() return client_sock, srv_sock def test_can_add_reader(loop, sock_pair): """Verify that we can add a reader callback to an event loop.""" def can_read(): if fut.done(): return data = srv_sock.recv(1) if len(data) != 1: return nonlocal got_msg got_msg = data # Indicate that we're done fut.set_result(None) srv_sock.close() def write(): client_sock.send(ref_msg) client_sock.close() ref_msg = b"a" client_sock, srv_sock = sock_pair loop.call_soon(write) exp_num_notifiers = len(loop._read_notifiers) + 1 got_msg = None fut = asyncio.Future() loop._add_reader(srv_sock.fileno(), can_read) assert len(loop._read_notifiers) == exp_num_notifiers, "Notifier should be added" loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) assert got_msg == ref_msg def test_can_remove_reader(loop, sock_pair): """Verify that we can remove a reader callback from an event loop.""" def can_read(): data = srv_sock.recv(1) if len(data) != 1: return nonlocal got_msg got_msg = data client_sock, srv_sock = sock_pair got_msg = None loop._add_reader(srv_sock.fileno(), can_read) exp_num_notifiers = len(loop._read_notifiers) - 1 loop._remove_reader(srv_sock.fileno()) assert len(loop._read_notifiers) == exp_num_notifiers, "Notifier should be removed" client_sock.send(b"a") client_sock.close() # Run for a short while to see if we get a read notification loop.call_later(0.1, loop.stop) loop.run_forever() assert got_msg is None, "Should not have received a read notification" def test_remove_reader_after_closing(loop, sock_pair): """Verify that we can remove a reader callback from an event loop.""" client_sock, srv_sock = sock_pair loop._add_reader(srv_sock.fileno(), lambda: None) loop.close() loop._remove_reader(srv_sock.fileno()) def test_remove_writer_after_closing(loop, sock_pair): """Verify that we can remove a reader callback from an event loop.""" client_sock, srv_sock = sock_pair loop._add_writer(client_sock.fileno(), lambda: None) loop.close() loop._remove_writer(client_sock.fileno()) def test_add_reader_after_closing(loop, sock_pair): """Verify that we can remove a reader callback from an event loop.""" client_sock, srv_sock = sock_pair loop.close() with pytest.raises(RuntimeError): loop._add_reader(srv_sock.fileno(), lambda: None) def test_add_writer_after_closing(loop, sock_pair): """Verify that we can remove a reader callback from an event loop.""" client_sock, srv_sock = sock_pair loop.close() with pytest.raises(RuntimeError): loop._add_writer(client_sock.fileno(), lambda: None) def test_can_add_writer(loop, sock_pair): """Verify that we can add a writer callback to an event loop.""" def can_write(): if not fut.done(): # Indicate that we're done fut.set_result(None) client_sock.close() client_sock, _ = sock_pair fut = asyncio.Future() loop._add_writer(client_sock.fileno(), can_write) assert len(loop._write_notifiers) == 1, "Notifier should be added" loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) def test_can_remove_writer(loop, sock_pair): """Verify that we can remove a writer callback from an event loop.""" client_sock, _ = sock_pair loop._add_writer(client_sock.fileno(), lambda: None) loop._remove_writer(client_sock.fileno()) assert not loop._write_notifiers, "Notifier should be removed" def test_add_reader_should_disable_qsocket_notifier_on_callback(loop, sock_pair): """Verify that add_reader disables QSocketNotifier during callback.""" def can_read(): nonlocal num_calls num_calls += 1 if num_calls == 2: # Since we get called again, the QSocketNotifier should've been re-enabled before # this call (although disabled during) assert not notifier.isEnabled() srv_sock.recv(1) fut.set_result(None) srv_sock.close() return assert not notifier.isEnabled() def write(): client_sock.send(b"a") client_sock.close() num_calls = 0 client_sock, srv_sock = sock_pair loop.call_soon(write) fut = asyncio.Future() loop._add_reader(srv_sock.fileno(), can_read) notifier = loop._read_notifiers[srv_sock.fileno()] loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) def test_add_writer_should_disable_qsocket_notifier_on_callback(loop, sock_pair): """Verify that add_writer disables QSocketNotifier during callback.""" def can_write(): nonlocal num_calls num_calls += 1 if num_calls == 2: # Since we get called again, the QSocketNotifier should've been re-enabled before # this call (although disabled during) assert not notifier.isEnabled() fut.set_result(None) client_sock.close() return assert not notifier.isEnabled() num_calls = 0 client_sock, _ = sock_pair fut = asyncio.Future() loop._add_writer(client_sock.fileno(), can_write) notifier = loop._write_notifiers[client_sock.fileno()] loop.run_until_complete(asyncio.wait_for(fut, timeout=1.0)) def test_reader_writer_echo(loop, sock_pair): """Verify readers and writers can send data to each other.""" c_sock, s_sock = sock_pair async def mycoro(): c_reader, c_writer = await asyncio.open_connection(sock=c_sock) s_reader, s_writer = await asyncio.open_connection(sock=s_sock) data = b"Echo... Echo... Echo..." s_writer.write(data) await s_writer.drain() read_data = await c_reader.readexactly(len(data)) assert data == read_data s_writer.close() loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=1.0)) def test_regression_bug13(loop, sock_pair): """Verify that a simple handshake between client and server works as expected.""" c_sock, s_sock = sock_pair client_done, server_done = asyncio.Future(), asyncio.Future() async def server_coro(): s_reader, s_writer = await asyncio.open_connection(sock=s_sock) s_writer.write(b"1") await s_writer.drain() assert (await s_reader.readexactly(1)) == b"2" s_writer.write(b"3") await s_writer.drain() server_done.set_result(True) result1 = None result3 = None async def client_coro(): def cb1(): nonlocal result1 assert result1 is None loop._remove_reader(c_sock.fileno()) result1 = c_sock.recv(1) loop._add_writer(c_sock.fileno(), cb2) def cb2(): nonlocal result3 assert result3 is None c_sock.send(b"2") loop._remove_writer(c_sock.fileno()) loop._add_reader(c_sock.fileno(), cb3) def cb3(): nonlocal result3 assert result3 is None result3 = c_sock.recv(1) loop._remove_reader(c_sock.fileno()) client_done.set_result(True) loop._add_reader(c_sock.fileno(), cb1) _client_task = asyncio.ensure_future(client_coro()) _server_task = asyncio.ensure_future(server_coro()) both_done = asyncio.gather(client_done, server_done) loop.run_until_complete(asyncio.wait_for(both_done, timeout=1.0)) assert result1 == b"1" assert result3 == b"3" def test_add_reader_replace(loop, sock_pair): c_sock, s_sock = sock_pair callback_invoked = asyncio.Future() called1 = False called2 = False def any_callback(): if not callback_invoked.done(): callback_invoked.set_result(True) loop._remove_reader(c_sock.fileno()) def callback1(): # the "bad" callback: if this gets invoked, something went wrong nonlocal called1 called1 = True any_callback() def callback2(): # the "good" callback: this is the one which should get called nonlocal called2 called2 = True any_callback() async def server_coro(): s_reader, s_writer = await asyncio.open_connection(sock=s_sock) s_writer.write(b"foo") await s_writer.drain() async def client_coro(): loop._add_reader(c_sock.fileno(), callback1) loop._add_reader(c_sock.fileno(), callback2) await callback_invoked loop._remove_reader(c_sock.fileno()) assert (await loop.sock_recv(c_sock, 3)) == b"foo" client_done = asyncio.ensure_future(client_coro()) server_done = asyncio.ensure_future(server_coro()) both_done = asyncio.wait( [server_done, client_done], return_when=asyncio.FIRST_EXCEPTION ) loop.run_until_complete(asyncio.wait_for(both_done, timeout=0.1)) assert not called1 assert called2 def test_add_writer_replace(loop, sock_pair): c_sock, s_sock = sock_pair callback_invoked = asyncio.Future() called1 = False called2 = False def any_callback(): if not callback_invoked.done(): callback_invoked.set_result(True) loop._remove_writer(c_sock.fileno()) def callback1(): # the "bad" callback: if this gets invoked, something went wrong nonlocal called1 called1 = True any_callback() def callback2(): # the "good" callback: this is the one which should get called nonlocal called2 called2 = True any_callback() async def client_coro(): loop._add_writer(c_sock.fileno(), callback1) loop._add_writer(c_sock.fileno(), callback2) await callback_invoked loop._remove_writer(c_sock.fileno()) loop.run_until_complete(asyncio.wait_for(client_coro(), timeout=0.1)) assert not called1 assert called2 def test_remove_reader_idempotence(loop, sock_pair): fd = sock_pair[0].fileno() def cb(): pass removed0 = loop._remove_reader(fd) loop._add_reader(fd, cb) removed1 = loop._remove_reader(fd) removed2 = loop._remove_reader(fd) assert not removed0 assert removed1 assert not removed2 def test_remove_writer_idempotence(loop, sock_pair): fd = sock_pair[0].fileno() def cb(): pass removed0 = loop._remove_writer(fd) loop._add_writer(fd, cb) removed1 = loop._remove_writer(fd) removed2 = loop._remove_writer(fd) assert not removed0 assert removed1 assert not removed2 def test_scheduling(loop, sock_pair): s1, s2 = sock_pair fd = s1.fileno() cb_called = asyncio.Future() def writer_cb(fut): if fut.done(): cb_called.set_exception(ValueError("writer_cb called twice")) fut.set_result(None) def fut_cb(fut): loop._remove_writer(fd) cb_called.set_result(None) fut = asyncio.Future() fut.add_done_callback(fut_cb) loop._add_writer(fd, writer_cb, fut) loop.run_until_complete(cb_called) @pytest.mark.xfail( "sys.version_info < (3,4)", reason="Doesn't work on python older than 3.4", ) def test_exception_handler(loop): handler_called = False coro_run = False loop.set_debug(False) async def future_except(): nonlocal coro_run coro_run = True loop.stop() raise ExceptionTester() def exct_handler(loop, data): nonlocal handler_called handler_called = True loop.set_exception_handler(exct_handler) asyncio.ensure_future(future_except()) loop.run_forever() assert coro_run assert handler_called def test_exception_handler_simple(loop): handler_called = False def exct_handler(loop, data): nonlocal handler_called handler_called = True loop.set_exception_handler(exct_handler) fut1 = asyncio.Future() fut1.set_exception(ExceptionTester()) asyncio.ensure_future(fut1) del fut1 loop.call_later(0.1, loop.stop) loop.run_forever() assert handler_called def test_not_running_immediately_after_stopped(loop): async def mycoro(): assert loop.is_running() await asyncio.sleep(0) loop.stop() assert not loop.is_running() assert not loop.is_running() loop.run_until_complete(mycoro()) assert not loop.is_running() @pytest.mark.parametrize( "async_wrap, expect_async_called, expect_exception", [(False, False, True), (True, True, False)], ) def test_async_wrap( loop, application, async_wrap, expect_async_called, expect_exception ): """ Re-entering the event loop from a Task will fail if there is another runnable task. """ async_called = False main_called = False async def async_job(): nonlocal async_called async_called = True def sync_callback(): coro = async_job() asyncio.create_task(coro) assert not async_called application.processEvents() assert async_called if expect_async_called else not async_called return 1, coro async def main(): nonlocal main_called if async_wrap: res, coro = await qasync.asyncWrap(sync_callback) else: res, coro = sync_callback() if expect_exception: await coro # avoid warnings about unawaited coroutines assert res == 1 main_called = True exceptions = [] loop.set_exception_handler(lambda loop, context: exceptions.append(context)) loop.run_until_complete(main()) assert main_called, "The main function should have been called" if expect_exception: # We will now have an error in there, because the task 'async_job' could not # be entered, because the task 'main' was still being executed by the event loop. assert len(exceptions) == 1 assert isinstance(exceptions[0]["exception"], RuntimeError) else: assert len(exceptions) == 0 def test_slow_callback_duration_logging(loop, caplog): async def mycoro(): time.sleep(1) caplog.clear() loop.set_debug(True) loop.slow_callback_duration = 0.1 with caplog.at_level(logging.WARNING): loop.run_until_complete(mycoro()) assert len(caplog.records) == 1 msg = caplog.records[0].message assert "Executing" in msg assert "took" in msg assert "seconds" in msg def test_run_until_complete_returns_future_result(loop): async def coro(): await asyncio.sleep(0) return 42 assert loop.run_until_complete(asyncio.wait_for(coro(), timeout=1)) == 42 def test_run_forever_custom_exit_code(loop, application): if hasattr(application, "exec"): orig_exec = application.exec application.exec = lambda: 42 try: assert loop.run_forever() == 42 finally: application.exec = orig_exec else: orig_exec = application.exec_ application.exec_ = lambda: 42 try: assert loop.run_forever() == 42 finally: application.exec_ = orig_exec def test_qeventloop_in_qthread(): class CoroutineExecutorThread(qasync.QtCore.QThread): def __init__(self, coro): super().__init__() self.coro = coro self.loop = None def run(self): self.loop = qasync.QEventLoop(self) asyncio.set_event_loop(self.loop) asyncio.run(self.coro) def join(self): self.loop.stop() self.loop.close() self.wait() event = threading.Event() async def coro(): await asyncio.sleep(0.1) event.set() thread = CoroutineExecutorThread(coro()) thread.start() assert event.wait(timeout=1), "Coroutine did not execute successfully" thread.join() # Ensure thread cleanup def teardown_module(module): """ Remove handlers from all loggers See: https://github.com/pytest-dev/pytest/issues/5502 """ loggers = [logging.getLogger()] + list(logging.Logger.manager.loggerDict.values()) for logger in loggers: handlers = getattr(logger, "handlers", []) for handler in handlers: if isinstance(logger, logging.Logger): logger.removeHandler(handler) qasync-0.28.0/tests/test_qthreadexec.py000066400000000000000000000061521505373037500201630ustar00rootroot00000000000000# © 2018 Gerard Marull-Paretas # © 2014 Mark Harviston # © 2014 Arve Knudsen # BSD License import logging import threading import weakref import pytest import qasync _TestObject = type("_TestObject", (object,), {}) @pytest.fixture def disable_executor_logging(): """ When running under pytest, leftover LogRecord objects keep references to objects in the scope that logging was called in. To avoid issues with tests targeting stale references, we disable logging for QThreadExecutor and _QThreadWorker classes. """ for cls in (qasync.QThreadExecutor, qasync._QThreadWorker): logger_name = cls.__qualname__ if cls.__module__ is not None: logger_name = f"{cls.__module__}.{logger_name}" logger = logging.getLogger(logger_name) logger.addHandler(logging.NullHandler()) logger.propagate = False @pytest.fixture def executor(request): exe = qasync.QThreadExecutor(5) request.addfinalizer(exe.shutdown) return exe @pytest.fixture def shutdown_executor(): exe = qasync.QThreadExecutor(5) exe.shutdown() return exe def test_shutdown_after_shutdown(shutdown_executor): with pytest.raises(RuntimeError): shutdown_executor.shutdown() def test_ctx_after_shutdown(shutdown_executor): with pytest.raises(RuntimeError): with shutdown_executor: pass def test_submit_after_shutdown(shutdown_executor): with pytest.raises(RuntimeError): shutdown_executor.submit(None) def test_stack_recursion_limit(executor): # Test that worker threads have sufficient stack size for the default # sys.getrecursionlimit. If not this should fail with SIGSEGV or SIGBUS # (or event SIGILL?) def rec(a, *args, **kwargs): rec(a, *args, **kwargs) fs = [executor.submit(rec, 1) for _ in range(10)] for f in fs: with pytest.raises(RecursionError): f.result() def test_no_stale_reference_as_argument(executor, disable_executor_logging): test_obj = _TestObject() test_obj_collected = threading.Event() # Reference to weakref has to be kept for callback to work _ = weakref.ref(test_obj, lambda *_: test_obj_collected.set()) # Submit object as argument to the executor future = executor.submit(lambda *_: None, test_obj) del test_obj # Wait for future to resolve future.result() collected = test_obj_collected.wait(timeout=1) assert collected is True, ( "Stale reference to executor argument not collected within timeout." ) def test_no_stale_reference_as_result(executor, disable_executor_logging): # Get object as result out of executor test_obj = executor.submit(lambda: _TestObject()).result() test_obj_collected = threading.Event() # Reference to weakref has to be kept for callback to work _ = weakref.ref(test_obj, lambda *_: test_obj_collected.set()) del test_obj collected = test_obj_collected.wait(timeout=1) assert collected is True, ( "Stale reference to executor result not collected within timeout." ) qasync-0.28.0/tests/test_run.py000066400000000000000000000053761505373037500165010ustar00rootroot00000000000000import asyncio import sys from unittest.mock import ANY import pytest import qasync @pytest.fixture def get_event_loop_coro(): async def coro(expected_debug): event_loop = asyncio.get_event_loop() assert type(event_loop) is qasync.QEventLoop assert event_loop.get_debug() == expected_debug await asyncio.sleep(0) return coro def test_qasync_run_restores_loop(get_event_loop_coro): asyncio.set_event_loop(None) qasync.run(get_event_loop_coro(ANY)) with pytest.raises(RuntimeError): _ = asyncio.get_event_loop() @pytest.mark.skipif(sys.version_info >= (3, 14), reason="Deprecated since Python 3.14") def test_qasync_run_restores_policy(get_event_loop_coro): old_policy = asyncio.get_event_loop_policy() qasync.run(get_event_loop_coro(ANY)) new_policy = asyncio.get_event_loop_policy() assert type(old_policy) is type(new_policy) def test_qasync_run_with_debug_args(get_event_loop_coro): qasync.run(get_event_loop_coro(True), debug=True) qasync.run(get_event_loop_coro(False), debug=False) @pytest.mark.skipif(sys.version_info < (3, 12), reason="Requires Python 3.12+") def test_asyncio_run(application): """Test that QEventLoop is compatible with asyncio.run()""" done = False loop = None async def main(): nonlocal done, loop assert loop.is_running() assert asyncio.get_running_loop() is loop await asyncio.sleep(0.01) done = True def factory(): nonlocal loop loop = qasync.QEventLoop(application) return loop asyncio.run(main(), loop_factory=factory) assert done assert loop.is_closed() assert not loop.is_running() @pytest.mark.skipif(sys.version_info < (3, 12), reason="Requires Python 3.12+") def test_asyncio_run_cleanup(application): """Test that running tasks are cleaned up""" task = None cancelled = False async def main(): nonlocal task, cancelled async def long_task(): nonlocal cancelled try: await asyncio.sleep(10) except asyncio.CancelledError: cancelled = True task = asyncio.create_task(long_task()) await asyncio.sleep(0.01) asyncio.run(main(), loop_factory=lambda: qasync.QEventLoop(application)) assert cancelled def test_qasync_run(application): """Test running with qasync.run()""" done = False loop = None async def main(): nonlocal done, loop loop = asyncio.get_running_loop() assert loop.is_running() await asyncio.sleep(0.01) done = True # qasync.run uses an EventLoopPolicy to create the loop qasync.run(main()) assert done assert loop.is_closed() assert not loop.is_running() qasync-0.28.0/uv.lock000066400000000000000000003006141505373037500144170ustar00rootroot00000000000000version = 1 revision = 2 requires-python = ">=3.8" resolution-markers = [ "python_full_version >= '3.9'", "python_full_version < '3.9'", ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, ] [[package]] name = "coverage" version = "7.10.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/87/0e/66dbd4c6a7f0758a8d18044c048779ba21fb94856e1edcf764bd5403e710/coverage-7.10.1.tar.gz", hash = "sha256:ae2b4856f29ddfe827106794f3589949a57da6f0d38ab01e24ec35107979ba57", size = 819938, upload-time = "2025-07-27T14:13:39.045Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/e7/0f4e35a15361337529df88151bddcac8e8f6d6fd01da94a4b7588901c2fe/coverage-7.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1c86eb388bbd609d15560e7cc0eb936c102b6f43f31cf3e58b4fd9afe28e1372", size = 214627, upload-time = "2025-07-27T14:11:01.211Z" }, { url = "https://files.pythonhosted.org/packages/e0/fd/17872e762c408362072c936dbf3ca28c67c609a1f5af434b1355edcb7e12/coverage-7.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b4ba0f488c1bdb6bd9ba81da50715a372119785458831c73428a8566253b86b", size = 215015, upload-time = "2025-07-27T14:11:03.988Z" }, { url = "https://files.pythonhosted.org/packages/54/50/c9d445ba38ee5f685f03876c0f8223469e2e46c5d3599594dca972b470c8/coverage-7.10.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083442ecf97d434f0cb3b3e3676584443182653da08b42e965326ba12d6b5f2a", size = 241995, upload-time = "2025-07-27T14:11:05.983Z" }, { url = "https://files.pythonhosted.org/packages/cc/83/4ae6e0f60376af33de543368394d21b9ac370dc86434039062ef171eebf8/coverage-7.10.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c1a40c486041006b135759f59189385da7c66d239bad897c994e18fd1d0c128f", size = 243253, upload-time = "2025-07-27T14:11:07.424Z" }, { url = "https://files.pythonhosted.org/packages/49/90/17a4d9ac7171be364ce8c0bb2b6da05e618ebfe1f11238ad4f26c99f5467/coverage-7.10.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3beb76e20b28046989300c4ea81bf690df84ee98ade4dc0bbbf774a28eb98440", size = 245110, upload-time = "2025-07-27T14:11:09.152Z" }, { url = "https://files.pythonhosted.org/packages/e1/f7/edc3f485d536ed417f3af2b4969582bcb5fab456241721825fa09354161e/coverage-7.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc265a7945e8d08da28999ad02b544963f813a00f3ed0a7a0ce4165fd77629f8", size = 243056, upload-time = "2025-07-27T14:11:10.586Z" }, { url = "https://files.pythonhosted.org/packages/58/2c/c4c316a57718556b8d0cc8304437741c31b54a62934e7c8c551a7915c2f4/coverage-7.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:47c91f32ba4ac46f1e224a7ebf3f98b4b24335bad16137737fe71a5961a0665c", size = 241731, upload-time = "2025-07-27T14:11:12.145Z" }, { url = "https://files.pythonhosted.org/packages/f7/93/c78e144c6f086043d0d7d9237c5b880e71ac672ed2712c6f8cca5544481f/coverage-7.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a108dd78ed185020f66f131c60078f3fae3f61646c28c8bb4edd3fa121fc7fc", size = 242023, upload-time = "2025-07-27T14:11:13.573Z" }, { url = "https://files.pythonhosted.org/packages/8f/e1/34e8505ca81fc144a612e1cc79fadd4a78f42e96723875f4e9f1f470437e/coverage-7.10.1-cp310-cp310-win32.whl", hash = "sha256:7092cc82382e634075cc0255b0b69cb7cada7c1f249070ace6a95cb0f13548ef", size = 217130, upload-time = "2025-07-27T14:11:15.11Z" }, { url = "https://files.pythonhosted.org/packages/75/2b/82adfce6edffc13d804aee414e64c0469044234af9296e75f6d13f92f6a2/coverage-7.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:ac0c5bba938879c2fc0bc6c1b47311b5ad1212a9dcb8b40fe2c8110239b7faed", size = 218015, upload-time = "2025-07-27T14:11:16.836Z" }, { url = "https://files.pythonhosted.org/packages/20/8e/ef088112bd1b26e2aa931ee186992b3e42c222c64f33e381432c8ee52aae/coverage-7.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b45e2f9d5b0b5c1977cb4feb5f594be60eb121106f8900348e29331f553a726f", size = 214747, upload-time = "2025-07-27T14:11:18.217Z" }, { url = "https://files.pythonhosted.org/packages/2d/76/a1e46f3c6e0897758eb43af88bb3c763cb005f4950769f7b553e22aa5f89/coverage-7.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a7a4d74cb0f5e3334f9aa26af7016ddb94fb4bfa11b4a573d8e98ecba8c34f1", size = 215128, upload-time = "2025-07-27T14:11:19.706Z" }, { url = "https://files.pythonhosted.org/packages/78/4d/903bafb371a8c887826ecc30d3977b65dfad0e1e66aa61b7e173de0828b0/coverage-7.10.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d4b0aab55ad60ead26159ff12b538c85fbab731a5e3411c642b46c3525863437", size = 245140, upload-time = "2025-07-27T14:11:21.261Z" }, { url = "https://files.pythonhosted.org/packages/55/f1/1f8f09536f38394a8698dd08a0e9608a512eacee1d3b771e2d06397f77bf/coverage-7.10.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dcc93488c9ebd229be6ee1f0d9aad90da97b33ad7e2912f5495804d78a3cd6b7", size = 246977, upload-time = "2025-07-27T14:11:23.15Z" }, { url = "https://files.pythonhosted.org/packages/57/cc/ed6bbc5a3bdb36ae1bca900bbbfdcb23b260ef2767a7b2dab38b92f61adf/coverage-7.10.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa309df995d020f3438407081b51ff527171cca6772b33cf8f85344b8b4b8770", size = 249140, upload-time = "2025-07-27T14:11:24.743Z" }, { url = "https://files.pythonhosted.org/packages/10/f5/e881ade2d8e291b60fa1d93d6d736107e940144d80d21a0d4999cff3642f/coverage-7.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cfb8b9d8855c8608f9747602a48ab525b1d320ecf0113994f6df23160af68262", size = 246869, upload-time = "2025-07-27T14:11:26.156Z" }, { url = "https://files.pythonhosted.org/packages/53/b9/6a5665cb8996e3cd341d184bb11e2a8edf01d8dadcf44eb1e742186cf243/coverage-7.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:320d86da829b012982b414c7cdda65f5d358d63f764e0e4e54b33097646f39a3", size = 244899, upload-time = "2025-07-27T14:11:27.622Z" }, { url = "https://files.pythonhosted.org/packages/27/11/24156776709c4e25bf8a33d6bb2ece9a9067186ddac19990f6560a7f8130/coverage-7.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dc60ddd483c556590da1d9482a4518292eec36dd0e1e8496966759a1f282bcd0", size = 245507, upload-time = "2025-07-27T14:11:29.544Z" }, { url = "https://files.pythonhosted.org/packages/43/db/a6f0340b7d6802a79928659c9a32bc778ea420e87a61b568d68ac36d45a8/coverage-7.10.1-cp311-cp311-win32.whl", hash = "sha256:4fcfe294f95b44e4754da5b58be750396f2b1caca8f9a0e78588e3ef85f8b8be", size = 217167, upload-time = "2025-07-27T14:11:31.349Z" }, { url = "https://files.pythonhosted.org/packages/f5/6f/1990eb4fd05cea4cfabdf1d587a997ac5f9a8bee883443a1d519a2a848c9/coverage-7.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:efa23166da3fe2915f8ab452dde40319ac84dc357f635737174a08dbd912980c", size = 218054, upload-time = "2025-07-27T14:11:33.202Z" }, { url = "https://files.pythonhosted.org/packages/b4/4d/5e061d6020251b20e9b4303bb0b7900083a1a384ec4e5db326336c1c4abd/coverage-7.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:d12b15a8c3759e2bb580ffa423ae54be4f184cf23beffcbd641f4fe6e1584293", size = 216483, upload-time = "2025-07-27T14:11:34.663Z" }, { url = "https://files.pythonhosted.org/packages/a5/3f/b051feeb292400bd22d071fdf933b3ad389a8cef5c80c7866ed0c7414b9e/coverage-7.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b7dc7f0a75a7eaa4584e5843c873c561b12602439d2351ee28c7478186c4da4", size = 214934, upload-time = "2025-07-27T14:11:36.096Z" }, { url = "https://files.pythonhosted.org/packages/f8/e4/a61b27d5c4c2d185bdfb0bfe9d15ab4ac4f0073032665544507429ae60eb/coverage-7.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:607f82389f0ecafc565813aa201a5cade04f897603750028dd660fb01797265e", size = 215173, upload-time = "2025-07-27T14:11:38.005Z" }, { url = "https://files.pythonhosted.org/packages/8a/01/40a6ee05b60d02d0bc53742ad4966e39dccd450aafb48c535a64390a3552/coverage-7.10.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f7da31a1ba31f1c1d4d5044b7c5813878adae1f3af8f4052d679cc493c7328f4", size = 246190, upload-time = "2025-07-27T14:11:39.887Z" }, { url = "https://files.pythonhosted.org/packages/11/ef/a28d64d702eb583c377255047281305dc5a5cfbfb0ee36e721f78255adb6/coverage-7.10.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51fe93f3fe4f5d8483d51072fddc65e717a175490804e1942c975a68e04bf97a", size = 248618, upload-time = "2025-07-27T14:11:41.841Z" }, { url = "https://files.pythonhosted.org/packages/6a/ad/73d018bb0c8317725370c79d69b5c6e0257df84a3b9b781bda27a438a3be/coverage-7.10.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e59d00830da411a1feef6ac828b90bbf74c9b6a8e87b8ca37964925bba76dbe", size = 250081, upload-time = "2025-07-27T14:11:43.705Z" }, { url = "https://files.pythonhosted.org/packages/2d/dd/496adfbbb4503ebca5d5b2de8bed5ec00c0a76558ffc5b834fd404166bc9/coverage-7.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:924563481c27941229cb4e16eefacc35da28563e80791b3ddc5597b062a5c386", size = 247990, upload-time = "2025-07-27T14:11:45.244Z" }, { url = "https://files.pythonhosted.org/packages/18/3c/a9331a7982facfac0d98a4a87b36ae666fe4257d0f00961a3a9ef73e015d/coverage-7.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ca79146ee421b259f8131f153102220b84d1a5e6fb9c8aed13b3badfd1796de6", size = 246191, upload-time = "2025-07-27T14:11:47.093Z" }, { url = "https://files.pythonhosted.org/packages/62/0c/75345895013b83f7afe92ec595e15a9a525ede17491677ceebb2ba5c3d85/coverage-7.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b225a06d227f23f386fdc0eab471506d9e644be699424814acc7d114595495f", size = 247400, upload-time = "2025-07-27T14:11:48.643Z" }, { url = "https://files.pythonhosted.org/packages/e2/a9/98b268cfc5619ef9df1d5d34fee408ecb1542d9fd43d467e5c2f28668cd4/coverage-7.10.1-cp312-cp312-win32.whl", hash = "sha256:5ba9a8770effec5baaaab1567be916c87d8eea0c9ad11253722d86874d885eca", size = 217338, upload-time = "2025-07-27T14:11:50.258Z" }, { url = "https://files.pythonhosted.org/packages/fe/31/22a5440e4d1451f253c5cd69fdcead65e92ef08cd4ec237b8756dc0b20a7/coverage-7.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:9eb245a8d8dd0ad73b4062135a251ec55086fbc2c42e0eb9725a9b553fba18a3", size = 218125, upload-time = "2025-07-27T14:11:52.034Z" }, { url = "https://files.pythonhosted.org/packages/d6/2b/40d9f0ce7ee839f08a43c5bfc9d05cec28aaa7c9785837247f96cbe490b9/coverage-7.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:7718060dd4434cc719803a5e526838a5d66e4efa5dc46d2b25c21965a9c6fcc4", size = 216523, upload-time = "2025-07-27T14:11:53.965Z" }, { url = "https://files.pythonhosted.org/packages/ef/72/135ff5fef09b1ffe78dbe6fcf1e16b2e564cd35faeacf3d63d60d887f12d/coverage-7.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebb08d0867c5a25dffa4823377292a0ffd7aaafb218b5d4e2e106378b1061e39", size = 214960, upload-time = "2025-07-27T14:11:55.959Z" }, { url = "https://files.pythonhosted.org/packages/b1/aa/73a5d1a6fc08ca709a8177825616aa95ee6bf34d522517c2595484a3e6c9/coverage-7.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f32a95a83c2e17422f67af922a89422cd24c6fa94041f083dd0bb4f6057d0bc7", size = 215220, upload-time = "2025-07-27T14:11:57.899Z" }, { url = "https://files.pythonhosted.org/packages/8d/40/3124fdd45ed3772a42fc73ca41c091699b38a2c3bd4f9cb564162378e8b6/coverage-7.10.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c4c746d11c8aba4b9f58ca8bfc6fbfd0da4efe7960ae5540d1a1b13655ee8892", size = 245772, upload-time = "2025-07-27T14:12:00.422Z" }, { url = "https://files.pythonhosted.org/packages/42/62/a77b254822efa8c12ad59e8039f2bc3df56dc162ebda55e1943e35ba31a5/coverage-7.10.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7f39edd52c23e5c7ed94e0e4bf088928029edf86ef10b95413e5ea670c5e92d7", size = 248116, upload-time = "2025-07-27T14:12:03.099Z" }, { url = "https://files.pythonhosted.org/packages/1d/01/8101f062f472a3a6205b458d18ef0444a63ae5d36a8a5ed5dd0f6167f4db/coverage-7.10.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab6e19b684981d0cd968906e293d5628e89faacb27977c92f3600b201926b994", size = 249554, upload-time = "2025-07-27T14:12:04.668Z" }, { url = "https://files.pythonhosted.org/packages/8f/7b/e51bc61573e71ff7275a4f167aecbd16cb010aefdf54bcd8b0a133391263/coverage-7.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5121d8cf0eacb16133501455d216bb5f99899ae2f52d394fe45d59229e6611d0", size = 247766, upload-time = "2025-07-27T14:12:06.234Z" }, { url = "https://files.pythonhosted.org/packages/4b/71/1c96d66a51d4204a9d6d12df53c4071d87e110941a2a1fe94693192262f5/coverage-7.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df1c742ca6f46a6f6cbcaef9ac694dc2cb1260d30a6a2f5c68c5f5bcfee1cfd7", size = 245735, upload-time = "2025-07-27T14:12:08.305Z" }, { url = "https://files.pythonhosted.org/packages/13/d5/efbc2ac4d35ae2f22ef6df2ca084c60e13bd9378be68655e3268c80349ab/coverage-7.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40f9a38676f9c073bf4b9194707aa1eb97dca0e22cc3766d83879d72500132c7", size = 247118, upload-time = "2025-07-27T14:12:09.903Z" }, { url = "https://files.pythonhosted.org/packages/d1/22/073848352bec28ca65f2b6816b892fcf9a31abbef07b868487ad15dd55f1/coverage-7.10.1-cp313-cp313-win32.whl", hash = "sha256:2348631f049e884839553b9974f0821d39241c6ffb01a418efce434f7eba0fe7", size = 217381, upload-time = "2025-07-27T14:12:11.535Z" }, { url = "https://files.pythonhosted.org/packages/b7/df/df6a0ff33b042f000089bd11b6bb034bab073e2ab64a56e78ed882cba55d/coverage-7.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:4072b31361b0d6d23f750c524f694e1a417c1220a30d3ef02741eed28520c48e", size = 218152, upload-time = "2025-07-27T14:12:13.182Z" }, { url = "https://files.pythonhosted.org/packages/30/e3/5085ca849a40ed6b47cdb8f65471c2f754e19390b5a12fa8abd25cbfaa8f/coverage-7.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:3e31dfb8271937cab9425f19259b1b1d1f556790e98eb266009e7a61d337b6d4", size = 216559, upload-time = "2025-07-27T14:12:14.807Z" }, { url = "https://files.pythonhosted.org/packages/cc/93/58714efbfdeb547909feaabe1d67b2bdd59f0597060271b9c548d5efb529/coverage-7.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1c4f679c6b573a5257af6012f167a45be4c749c9925fd44d5178fd641ad8bf72", size = 215677, upload-time = "2025-07-27T14:12:16.68Z" }, { url = "https://files.pythonhosted.org/packages/c0/0c/18eaa5897e7e8cb3f8c45e563e23e8a85686b4585e29d53cacb6bc9cb340/coverage-7.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:871ebe8143da284bd77b84a9136200bd638be253618765d21a1fce71006d94af", size = 215899, upload-time = "2025-07-27T14:12:18.758Z" }, { url = "https://files.pythonhosted.org/packages/84/c1/9d1affacc3c75b5a184c140377701bbf14fc94619367f07a269cd9e4fed6/coverage-7.10.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:998c4751dabf7d29b30594af416e4bf5091f11f92a8d88eb1512c7ba136d1ed7", size = 257140, upload-time = "2025-07-27T14:12:20.357Z" }, { url = "https://files.pythonhosted.org/packages/3d/0f/339bc6b8fa968c346df346068cca1f24bdea2ddfa93bb3dc2e7749730962/coverage-7.10.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:780f750a25e7749d0af6b3631759c2c14f45de209f3faaa2398312d1c7a22759", size = 259005, upload-time = "2025-07-27T14:12:22.007Z" }, { url = "https://files.pythonhosted.org/packages/c8/22/89390864b92ea7c909079939b71baba7e5b42a76bf327c1d615bd829ba57/coverage-7.10.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:590bdba9445df4763bdbebc928d8182f094c1f3947a8dc0fc82ef014dbdd8324", size = 261143, upload-time = "2025-07-27T14:12:23.746Z" }, { url = "https://files.pythonhosted.org/packages/2c/56/3d04d89017c0c41c7a71bd69b29699d919b6bbf2649b8b2091240b97dd6a/coverage-7.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b2df80cb6a2af86d300e70acb82e9b79dab2c1e6971e44b78dbfc1a1e736b53", size = 258735, upload-time = "2025-07-27T14:12:25.73Z" }, { url = "https://files.pythonhosted.org/packages/cb/40/312252c8afa5ca781063a09d931f4b9409dc91526cd0b5a2b84143ffafa2/coverage-7.10.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d6a558c2725bfb6337bf57c1cd366c13798bfd3bfc9e3dd1f4a6f6fc95a4605f", size = 256871, upload-time = "2025-07-27T14:12:27.767Z" }, { url = "https://files.pythonhosted.org/packages/1f/2b/564947d5dede068215aaddb9e05638aeac079685101462218229ddea9113/coverage-7.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e6150d167f32f2a54690e572e0a4c90296fb000a18e9b26ab81a6489e24e78dd", size = 257692, upload-time = "2025-07-27T14:12:29.347Z" }, { url = "https://files.pythonhosted.org/packages/93/1b/c8a867ade85cb26d802aea2209b9c2c80613b9c122baa8c8ecea6799648f/coverage-7.10.1-cp313-cp313t-win32.whl", hash = "sha256:d946a0c067aa88be4a593aad1236493313bafaa27e2a2080bfe88db827972f3c", size = 218059, upload-time = "2025-07-27T14:12:31.076Z" }, { url = "https://files.pythonhosted.org/packages/a1/fe/cd4ab40570ae83a516bf5e754ea4388aeedd48e660e40c50b7713ed4f930/coverage-7.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e37c72eaccdd5ed1130c67a92ad38f5b2af66eeff7b0abe29534225db2ef7b18", size = 219150, upload-time = "2025-07-27T14:12:32.746Z" }, { url = "https://files.pythonhosted.org/packages/8d/16/6e5ed5854be6d70d0c39e9cb9dd2449f2c8c34455534c32c1a508c7dbdb5/coverage-7.10.1-cp313-cp313t-win_arm64.whl", hash = "sha256:89ec0ffc215c590c732918c95cd02b55c7d0f569d76b90bb1a5e78aa340618e4", size = 217014, upload-time = "2025-07-27T14:12:34.406Z" }, { url = "https://files.pythonhosted.org/packages/54/8e/6d0bfe9c3d7121cf936c5f8b03e8c3da1484fb801703127dba20fb8bd3c7/coverage-7.10.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:166d89c57e877e93d8827dac32cedae6b0277ca684c6511497311249f35a280c", size = 214951, upload-time = "2025-07-27T14:12:36.069Z" }, { url = "https://files.pythonhosted.org/packages/f2/29/e3e51a8c653cf2174c60532aafeb5065cea0911403fa144c9abe39790308/coverage-7.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bed4a2341b33cd1a7d9ffc47df4a78ee61d3416d43b4adc9e18b7d266650b83e", size = 215229, upload-time = "2025-07-27T14:12:37.759Z" }, { url = "https://files.pythonhosted.org/packages/e0/59/3c972080b2fa18b6c4510201f6d4dc87159d450627d062cd9ad051134062/coverage-7.10.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddca1e4f5f4c67980533df01430184c19b5359900e080248bbf4ed6789584d8b", size = 245738, upload-time = "2025-07-27T14:12:39.453Z" }, { url = "https://files.pythonhosted.org/packages/2e/04/fc0d99d3f809452654e958e1788454f6e27b34e43f8f8598191c8ad13537/coverage-7.10.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:37b69226001d8b7de7126cad7366b0778d36777e4d788c66991455ba817c5b41", size = 248045, upload-time = "2025-07-27T14:12:41.387Z" }, { url = "https://files.pythonhosted.org/packages/5e/2e/afcbf599e77e0dfbf4c97197747250d13d397d27e185b93987d9eaac053d/coverage-7.10.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2f22102197bcb1722691296f9e589f02b616f874e54a209284dd7b9294b0b7f", size = 249666, upload-time = "2025-07-27T14:12:43.056Z" }, { url = "https://files.pythonhosted.org/packages/6e/ae/bc47f7f8ecb7a06cbae2bf86a6fa20f479dd902bc80f57cff7730438059d/coverage-7.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e0c768b0f9ac5839dac5cf88992a4bb459e488ee8a1f8489af4cb33b1af00f1", size = 247692, upload-time = "2025-07-27T14:12:44.83Z" }, { url = "https://files.pythonhosted.org/packages/b6/26/cbfa3092d31ccba8ba7647e4d25753263e818b4547eba446b113d7d1efdf/coverage-7.10.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:991196702d5e0b120a8fef2664e1b9c333a81d36d5f6bcf6b225c0cf8b0451a2", size = 245536, upload-time = "2025-07-27T14:12:46.527Z" }, { url = "https://files.pythonhosted.org/packages/56/77/9c68e92500e6a1c83d024a70eadcc9a173f21aadd73c4675fe64c9c43fdf/coverage-7.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae8e59e5f4fd85d6ad34c2bb9d74037b5b11be072b8b7e9986beb11f957573d4", size = 246954, upload-time = "2025-07-27T14:12:49.279Z" }, { url = "https://files.pythonhosted.org/packages/7f/a5/ba96671c5a669672aacd9877a5987c8551501b602827b4e84256da2a30a7/coverage-7.10.1-cp314-cp314-win32.whl", hash = "sha256:042125c89cf74a074984002e165d61fe0e31c7bd40ebb4bbebf07939b5924613", size = 217616, upload-time = "2025-07-27T14:12:51.214Z" }, { url = "https://files.pythonhosted.org/packages/e7/3c/e1e1eb95fc1585f15a410208c4795db24a948e04d9bde818fe4eb893bc85/coverage-7.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22c3bfe09f7a530e2c94c87ff7af867259c91bef87ed2089cd69b783af7b84e", size = 218412, upload-time = "2025-07-27T14:12:53.429Z" }, { url = "https://files.pythonhosted.org/packages/b0/85/7e1e5be2cb966cba95566ba702b13a572ca744fbb3779df9888213762d67/coverage-7.10.1-cp314-cp314-win_arm64.whl", hash = "sha256:ee6be07af68d9c4fca4027c70cea0c31a0f1bc9cb464ff3c84a1f916bf82e652", size = 216776, upload-time = "2025-07-27T14:12:55.482Z" }, { url = "https://files.pythonhosted.org/packages/62/0f/5bb8f29923141cca8560fe2217679caf4e0db643872c1945ac7d8748c2a7/coverage-7.10.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d24fb3c0c8ff0d517c5ca5de7cf3994a4cd559cde0315201511dbfa7ab528894", size = 215698, upload-time = "2025-07-27T14:12:57.225Z" }, { url = "https://files.pythonhosted.org/packages/80/29/547038ffa4e8e4d9e82f7dfc6d152f75fcdc0af146913f0ba03875211f03/coverage-7.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1217a54cfd79be20512a67ca81c7da3f2163f51bbfd188aab91054df012154f5", size = 215902, upload-time = "2025-07-27T14:12:59.071Z" }, { url = "https://files.pythonhosted.org/packages/e1/8a/7aaa8fbfaed900147987a424e112af2e7790e1ac9cd92601e5bd4e1ba60a/coverage-7.10.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:51f30da7a52c009667e02f125737229d7d8044ad84b79db454308033a7808ab2", size = 257230, upload-time = "2025-07-27T14:13:01.248Z" }, { url = "https://files.pythonhosted.org/packages/e5/1d/c252b5ffac44294e23a0d79dd5acf51749b39795ccc898faeabf7bee903f/coverage-7.10.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ed3718c757c82d920f1c94089066225ca2ad7f00bb904cb72b1c39ebdd906ccb", size = 259194, upload-time = "2025-07-27T14:13:03.247Z" }, { url = "https://files.pythonhosted.org/packages/16/ad/6c8d9f83d08f3bac2e7507534d0c48d1a4f52c18e6f94919d364edbdfa8f/coverage-7.10.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc452481e124a819ced0c25412ea2e144269ef2f2534b862d9f6a9dae4bda17b", size = 261316, upload-time = "2025-07-27T14:13:04.957Z" }, { url = "https://files.pythonhosted.org/packages/d6/4e/f9bbf3a36c061e2e0e0f78369c006d66416561a33d2bee63345aee8ee65e/coverage-7.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d6f494c307e5cb9b1e052ec1a471060f1dea092c8116e642e7a23e79d9388ea", size = 258794, upload-time = "2025-07-27T14:13:06.715Z" }, { url = "https://files.pythonhosted.org/packages/87/82/e600bbe78eb2cb0541751d03cef9314bcd0897e8eea156219c39b685f869/coverage-7.10.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fc0e46d86905ddd16b85991f1f4919028092b4e511689bbdaff0876bd8aab3dd", size = 256869, upload-time = "2025-07-27T14:13:08.933Z" }, { url = "https://files.pythonhosted.org/packages/ce/5d/2fc9a9236c5268f68ac011d97cd3a5ad16cc420535369bedbda659fdd9b7/coverage-7.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80b9ccd82e30038b61fc9a692a8dc4801504689651b281ed9109f10cc9fe8b4d", size = 257765, upload-time = "2025-07-27T14:13:10.778Z" }, { url = "https://files.pythonhosted.org/packages/8a/05/b4e00b2bd48a2dc8e1c7d2aea7455f40af2e36484ab2ef06deb85883e9fe/coverage-7.10.1-cp314-cp314t-win32.whl", hash = "sha256:e58991a2b213417285ec866d3cd32db17a6a88061a985dbb7e8e8f13af429c47", size = 218420, upload-time = "2025-07-27T14:13:12.882Z" }, { url = "https://files.pythonhosted.org/packages/77/fb/d21d05f33ea27ece327422240e69654b5932b0b29e7fbc40fbab3cf199bf/coverage-7.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e88dd71e4ecbc49d9d57d064117462c43f40a21a1383507811cf834a4a620651", size = 219536, upload-time = "2025-07-27T14:13:14.718Z" }, { url = "https://files.pythonhosted.org/packages/a6/68/7fea94b141281ed8be3d1d5c4319a97f2befc3e487ce33657fc64db2c45e/coverage-7.10.1-cp314-cp314t-win_arm64.whl", hash = "sha256:1aadfb06a30c62c2eb82322171fe1f7c288c80ca4156d46af0ca039052814bab", size = 217190, upload-time = "2025-07-27T14:13:16.85Z" }, { url = "https://files.pythonhosted.org/packages/c3/98/9b19d4aebfb31552596a7ac55cd678c3ebd74be6153888c56d39e23f376b/coverage-7.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:57b6e8789cbefdef0667e4a94f8ffa40f9402cee5fc3b8e4274c894737890145", size = 214625, upload-time = "2025-07-27T14:13:18.661Z" }, { url = "https://files.pythonhosted.org/packages/ea/24/e2391365d0940fc757666ecd7572aced0963e859188e57169bd18fba5d29/coverage-7.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:85b22a9cce00cb03156334da67eb86e29f22b5e93876d0dd6a98646bb8a74e53", size = 215001, upload-time = "2025-07-27T14:13:20.478Z" }, { url = "https://files.pythonhosted.org/packages/ce/0c/c1740d7fac57cb0c54cd04786f3dbfc4d0bfa0a6cc9f19f69c170ae67f6a/coverage-7.10.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97b6983a2f9c76d345ca395e843a049390b39652984e4a3b45b2442fa733992d", size = 241082, upload-time = "2025-07-27T14:13:22.318Z" }, { url = "https://files.pythonhosted.org/packages/45/b5/965b26315ecae6455bc40f1de8563a57e82cb31af8af2e2844655cf400f1/coverage-7.10.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ddf2a63b91399a1c2f88f40bc1705d5a7777e31c7e9eb27c602280f477b582ba", size = 242979, upload-time = "2025-07-27T14:13:24.123Z" }, { url = "https://files.pythonhosted.org/packages/0b/48/80c5c6a5a792348ba71b2315809c5a2daab2981564e31d1f3cd092c8cd97/coverage-7.10.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47ab6dbbc31a14c5486420c2c1077fcae692097f673cf5be9ddbec8cdaa4cdbc", size = 244550, upload-time = "2025-07-27T14:13:25.9Z" }, { url = "https://files.pythonhosted.org/packages/ab/73/332667b91cfa3c27130026af220fca478b07e913e96932d12c100e1a7314/coverage-7.10.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:21eb7d8b45d3700e7c2936a736f732794c47615a20f739f4133d5230a6512a88", size = 242482, upload-time = "2025-07-27T14:13:28.121Z" }, { url = "https://files.pythonhosted.org/packages/ae/e6/24c9120ad91314be82f793a2a174fe738583a716264b1523fe95ad731cb3/coverage-7.10.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:283005bb4d98ae33e45f2861cd2cde6a21878661c9ad49697f6951b358a0379b", size = 240717, upload-time = "2025-07-27T14:13:29.93Z" }, { url = "https://files.pythonhosted.org/packages/94/9a/21a4d5135eb4b8064fd9bf8a8eb8d4465982611d2d7fb569d6c2edf38f04/coverage-7.10.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fefe31d61d02a8b2c419700b1fade9784a43d726de26495f243b663cd9fe1513", size = 241669, upload-time = "2025-07-27T14:13:31.726Z" }, { url = "https://files.pythonhosted.org/packages/3f/1d/e4ce3b23f8b8b0fe196c436499414b1af06b9e1610cefedaaad37c9668d0/coverage-7.10.1-cp39-cp39-win32.whl", hash = "sha256:e8ab8e4c7ec7f8a55ac05b5b715a051d74eac62511c6d96d5bb79aaafa3b04cf", size = 217138, upload-time = "2025-07-27T14:13:33.481Z" }, { url = "https://files.pythonhosted.org/packages/d3/c6/b7fcf41c341e686610fdf9ef1a4b29045015f36d3eecd17679874e4739ed/coverage-7.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:c36baa0ecde742784aa76c2b816466d3ea888d5297fda0edbac1bf48fa94688a", size = 218035, upload-time = "2025-07-27T14:13:35.337Z" }, { url = "https://files.pythonhosted.org/packages/0f/64/922899cff2c0fd3496be83fa8b81230f5a8d82a2ad30f98370b133c2c83b/coverage-7.10.1-py3-none-any.whl", hash = "sha256:fa2a258aa6bf188eb9a8948f7102a83da7c430a0dce918dbd8b60ef8fcb772d7", size = 206597, upload-time = "2025-07-27T14:13:37.221Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "mypy" version = "1.14.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] dependencies = [ { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, { name = "tomli", marker = "python_full_version < '3.9'" }, { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, ] [[package]] name = "mypy" version = "1.17.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.9'", ] dependencies = [ { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, { name = "pathspec", marker = "python_full_version >= '3.9'" }, { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/31/e762baa3b73905c856d45ab77b4af850e8159dffffd86a52879539a08c6b/mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6", size = 10998313, upload-time = "2025-07-14T20:33:24.519Z" }, { url = "https://files.pythonhosted.org/packages/1c/c1/25b2f0d46fb7e0b5e2bee61ec3a47fe13eff9e3c2f2234f144858bbe6485/mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d", size = 10128922, upload-time = "2025-07-14T20:34:06.414Z" }, { url = "https://files.pythonhosted.org/packages/02/78/6d646603a57aa8a2886df1b8881fe777ea60f28098790c1089230cd9c61d/mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b", size = 11913524, upload-time = "2025-07-14T20:33:19.109Z" }, { url = "https://files.pythonhosted.org/packages/4f/19/dae6c55e87ee426fb76980f7e78484450cad1c01c55a1dc4e91c930bea01/mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a", size = 12650527, upload-time = "2025-07-14T20:32:44.095Z" }, { url = "https://files.pythonhosted.org/packages/86/e1/f916845a235235a6c1e4d4d065a3930113767001d491b8b2e1b61ca56647/mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f", size = 12897284, upload-time = "2025-07-14T20:33:38.168Z" }, { url = "https://files.pythonhosted.org/packages/ae/dc/414760708a4ea1b096bd214d26a24e30ac5e917ef293bc33cdb6fe22d2da/mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937", size = 9506493, upload-time = "2025-07-14T20:34:01.093Z" }, { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, { url = "https://files.pythonhosted.org/packages/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" }, { url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" }, { url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" }, { url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" }, { url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" }, { url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" }, { url = "https://files.pythonhosted.org/packages/9f/a0/6263dd11941231f688f0a8f2faf90ceac1dc243d148d314a089d2fe25108/mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab", size = 10988185, upload-time = "2025-07-14T20:33:04.797Z" }, { url = "https://files.pythonhosted.org/packages/02/13/b8f16d6b0dc80277129559c8e7dbc9011241a0da8f60d031edb0e6e9ac8f/mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad", size = 10120169, upload-time = "2025-07-14T20:32:38.84Z" }, { url = "https://files.pythonhosted.org/packages/14/ef/978ba79df0d65af680e20d43121363cf643eb79b04bf3880d01fc8afeb6f/mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c", size = 11918121, upload-time = "2025-07-14T20:33:52.328Z" }, { url = "https://files.pythonhosted.org/packages/f4/10/55ef70b104151a0d8280474f05268ff0a2a79be8d788d5e647257d121309/mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8", size = 12648821, upload-time = "2025-07-14T20:32:59.631Z" }, { url = "https://files.pythonhosted.org/packages/26/8c/7781fcd2e1eef48fbedd3a422c21fe300a8e03ed5be2eb4bd10246a77f4e/mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97", size = 12896955, upload-time = "2025-07-14T20:32:49.543Z" }, { url = "https://files.pythonhosted.org/packages/78/13/03ac759dabe86e98ca7b6681f114f90ee03f3ff8365a57049d311bd4a4e3/mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4", size = 9512957, upload-time = "2025-07-14T20:33:28.619Z" }, { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.9'", ] 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 = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pytest" version = "7.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, { name = "iniconfig", marker = "python_full_version < '3.9'" }, { name = "packaging", marker = "python_full_version < '3.9'" }, { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "tomli", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/f3/dadfbdbf6b6c8b5bd02adb1e08bc9fbb45ba51c68b0893fa536378cdf485/pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a", size = 1349733, upload-time = "2023-06-23T11:17:28.791Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/b2/741130cbcf2bbfa852ed95a60dc311c9e232c7ed25bac3d9b8880a8df4ae/pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", size = 323580, upload-time = "2023-06-23T11:17:25.738Z" }, ] [[package]] name = "pytest" version = "8.4.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.9'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, { name = "iniconfig", marker = "python_full_version >= '3.9'" }, { name = "packaging", marker = "python_full_version >= '3.9'" }, { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pygments", marker = "python_full_version >= '3.9'" }, { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] [[package]] name = "qasync" version = "0.27.1" source = { editable = "." } [package.optional-dependencies] typing = [ { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "mypy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] [package.dev-dependencies] dev = [ { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "coverage", version = "7.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest", version = "7.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] [package.metadata] requires-dist = [{ name = "mypy", marker = "extra == 'typing'", specifier = ">=1.0" }] provides-extras = ["typing"] [package.metadata.requires-dev] dev = [ { name = "coverage", marker = "python_full_version < '3.9'", specifier = "==7.6.1" }, { name = "coverage", marker = "python_full_version >= '3.9'", specifier = ">=7.10" }, { name = "pytest", marker = "python_full_version < '3.9'", specifier = "==7.4" }, { name = "pytest", marker = "python_full_version >= '3.9'", specifier = ">=8.4" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] name = "typing-extensions" version = "4.13.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, ] [[package]] name = "typing-extensions" version = "4.14.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ]