diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 0000000..7c51009 --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,3 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ diff --git a/.gitattributes b/.gitattributes index 6313b56..d1790b5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ * text=auto eol=lf +.git_archival.txt export-subst diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 42b341e..e9d08c8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,51 +1,82 @@ -name: Tests +name: CI on: + workflow_dispatch: + pull_request: push: branches: - main - pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_COLOR: 3 jobs: - tests: + pre-commit: + name: Format runs-on: ubuntu-latest steps: - - name: Check out the repository - uses: actions/checkout@v4 - - - name: Set up Python 3.10 - uses: actions/setup-python@v5.0.0 + - uses: actions/checkout@v4 with: - python-version: "3.10" - - - name: Cache poetry install - uses: actions/cache@v4 + fetch-depth: 0 + - uses: actions/setup-python@v5 with: - path: ~/.local - key: poetry-1.7.1-0 - - - name: Install poetry - uses: snok/install-poetry@v1 + python-version: "3.x" + - uses: pre-commit/action@v3.0.1 with: - version: 1.7.1 - virtualenvs-create: true - virtualenvs-in-project: true + extra_args: --hook-stage manual --all-files + - name: Run Ruff + run: | + pip install ruff + ruff check . - - name: cache deps - id: cache-deps - uses: actions/cache@v4 + tests: + name: Python ${{ matrix.python-version }} on ${{ matrix.runs-on }} + runs-on: ${{ matrix.runs-on }} + needs: [pre-commit] + strategy: + fail-fast: false + matrix: + python-version: ["3.11"] + # python-version: ["3.11", "3.12", "3.13"] + runs-on: [ubuntu-latest] + + steps: + - uses: actions/checkout@v4 with: - path: .venv - key: pydeps-${{ hashFiles('**/poetry.lock') }} + fetch-depth: 0 - - run: poetry install --no-interaction # --no-root - if: steps.cache-deps.outputs.cache-hit != 'true' + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true - # - run: poetry install --no-interaction + - name: Install dependencies + run: python -m pip install -e ".[test,docs]" - - run: poetry run pytest --cov --cov-report xml + - name: Run tests + run: python -m pytest -ra --cov --cov-report=xml --cov-report=term --durations=20 - - name: Upload coverage roports to Codecov + - name: Upload coverage uses: codecov/codecov-action@v4 env: - CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + files: ./coverage.xml + fail_ci_if_error: true + + docs: + runs-on: ubuntu-latest + needs: [pre-commit] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Build docs + run: | + pip install ".[docs]" + sphinx-build -b html docs docs/_build/html diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2815cd9..482d57b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: "v4.4.0" + rev: "v5.0.0" hooks: - id: check-case-conflict - id: check-merge-conflict @@ -10,10 +10,8 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.1.5 + rev: "v0.7.4" hooks: - # Run the linter. - id: ruff - # Run the formatter. + args: ["--fix", "--show-fixes"] - id: ruff-format diff --git a/.readthedocs.yml b/.readthedocs.yml index 66f2a21..67c194c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,12 +1,17 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + version: 2 + build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.10" -sphinx: - configuration: docs/conf.py -formats: all -python: - install: - - requirements: docs/requirements.txt - - path: . + python: "3.12" + commands: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + - uv venv + - uv pip install .[docs] + - .venv/bin/python -m sphinx -T -b html -d docs/_build/doctrees -D + language=en docs $READTHEDOCS_OUTPUT/html diff --git a/README.md b/README.md index 3f1f082..4a777e9 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,8 @@ Heisskleber is a versatile library designed to seamlessly "glue" together variou ## Features -- Multiple Protocol Support: Easy integration with zmq, mqtt, udp, serial, influxdb, and cmdline. Future plans include REST API and file operations. -- Custom Data Handling: Customizable "unpack" and "pack" functions allow for the translation of any data format (e.g., ascii encoded, comma-separated messages from a serial bus) into dictionaries for easy manipulation and transmission. -- Synchronous & Asynchronous Versions: Cater to different programming needs and scenarios with both sync and async interfaces. +- Multiple Protocol Support: Easy integration with zmq, mqtt, udp, serial, and cmdline. Future plans include REST API and file operations. +- Custom Data Handling: Customizable "unpacker" and "packer" functions allow for the translation of any data format (e.g., ascii encoded, comma-separated messages from a serial bus) into dictionaries for easy manipulation and transmission. - Extensible: Designed for easy extension with additional protocols and data handling functions. ## Installation @@ -36,56 +35,35 @@ You can install _Heisskleber_ via [pip] from [PyPI]: $ pip install heisskleber ``` -Configuration files for zmq, mqtt and other heisskleber related settings should be placed in the user's config directory, usually `$HOME/.config`. Config file templates can be found in the `config` -directory of the package. - ## Quick Start Here's a simple example to demonstrate how Heisskleber can be used to connect a zmq source to an mqtt sink: ```python """ -A simple forwarder that takes messages from +A simple forwarder that takes messages from a serial device and publishes them via MQTT. """ +import asyncio from heisskleber.serial import SerialSubscriber, SerialConf from heisskleber.mqtt import MqttPublisher, MqttConf -source = SerialSubscriber(config=SerialConf(port="/dev/ACM0")) -sink = MqttPublisher(config=MqttConf(host="127.0.0.1", port=1883, user="", password="")) -while True: - topic, data = source.receive() - sink.send(data, topic="/hostname/" + topic) +async def main(): + source = SerialSubscriber(config=SerialConf(port="/dev/ACM0", baudrate=9600)) + sink = MqttPublisher(config=MqttConf(host="mqtt.example.com", port=1883, user="", password="")) + + while True: + data, metadata = await source.receive() + await sink.send(data, topic="/hotglue/" + metadata.get("topic", "serial")) + +asyncio.run(main()) ``` -All sources and sinks come with customizable "unpack" and "pack" functions, making it simple to work with various data formats. - -It is also possible to do configuration via yaml files, placed at `$HOME/.config/heisskleber` and named according to the protocol in question. +All sources and sinks come with customizable "unpacker" and "packer" functions, making it simple to work with various data formats. See the [documentation][read the docs] for detailed usage. -## Development - -1. Install poetry - -``` -curl -sSL https://install.python-poetry.org | python3 - -``` - -2. clone repository - -``` -git clone https://github.com/flucto-gmbh/heisskleber.git -cd heisskleber -``` - -3. setup - -``` -make install -``` - ## License Distributed under the terms of the [MIT license][license], diff --git a/bin/zmq_broker.py b/bin/zmq_broker.py new file mode 100644 index 0000000..3f261a8 --- /dev/null +++ b/bin/zmq_broker.py @@ -0,0 +1,52 @@ +# /// script +# dependencies = [ +# "pyzmq", +# "heisskleber" +# ] +# /// + +import argparse +import logging +import sys + +import zmq + +from heisskleber.zmq import ZmqConf + +logger = logging.getLogger(__name__) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - ZmqBroker - %(levelname)s - %(message)s") + + +def main() -> None: + """Run ZMQ broker.""" + parser = argparse.ArgumentParser() + parser.add_argument("-c", "--config", type=str, required=True, help="ZMQ configuration file (yaml or json)") + + args = parser.parse_args() + + config = ZmqConf.from_file(args.config) + + try: + ctx = zmq.Context() + + logger.info("Creating XPUB socket") + xpub = ctx.socket(zmq.XPUB) + logger.info("Creating XSUB socket") + xsub = ctx.socket(zmq.XSUB) + + logger.info("Connecting XPUB socket to %(addr)s", {"addr": config.subscriber_address}) + xpub.bind(config.subscriber_address) + + logger.info("Connecting XSUB socket to %(addr)s", {"addr": config.publisher_address}) + xsub.bind(config.publisher_address) + + logger.info("Starting proxy...") + zmq.proxy(xpub, xsub) + except Exception: + logger.exception("Oh no! ZMQ broker failed!") + sys.exit(-1) + + +if __name__ == "__main__": + main() diff --git a/docs/conf.py b/docs/conf.py index 0a9423e..6f94003 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,12 +1,74 @@ """Sphinx configuration.""" + +from __future__ import annotations + +import importlib.metadata +from typing import Any + project = "Heisskleber" -author = "Felix Weiler" -copyright = "2023, Felix Weiler" +author = "Felix Weiler-Detjen" +copyright = "2023, Flucto GmbH" + +version = release = importlib.metadata.version("heisskleber") + extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", "myst_parser", -] # , "autodoc2" -# autodoc2_packages = ["../heisskleber"] -autodoc_typehints = "description" + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", + "sphinx_copybutton", +] + +autodoc_typehints = "description" # or 'signature' or 'both' + +autodoc_type_aliases = { + "T": "heisskleber.core.T", + "T_co": "heisskleber.core.T_co", + "T_contra": "heisskleber.core.T_contra", +} + +# If you're using typing.TypeVar in your code: +nitpicky = True +nitpick_ignore = [ + ("py:class", "T"), + ("py:class", "T_co"), + ("py:class", "T_contra"), + ("py:data", "typing.Any"), + ("py:class", "_io.StringIO"), + ("py:class", "_io.BytesIO"), +] + +source_suffix = [".rst", ".md"] + +exclude_patterns = [ + "_build", + "**.ipynb_checkpoints", + "Thumbs.db", + ".DS_Store", + ".env", + ".venv", +] + html_theme = "furo" + +html_theme_options: dict[str, Any] = { + "footer_icons": [ + { + "name": "GitHub", + "url": "https://github.com/flucto-gmbh/heisskleber", + "html": """ + + + + """, + "class": "", + }, + ], + "source_repository": "https://github.com/flucto-gmbh/heisskleber", + "source_branch": "main", + "source_directory": "docs/", +} + +always_document_param_types = True diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..3bcb81c --- /dev/null +++ b/docs/development.md @@ -0,0 +1,7 @@ +# How to contribute to development + +1. Fork repository +2. Set up development environment + +- Install uv, if you don't have it already: `curl -LsSf https://astral.sh/uv/install.sh | sh` (Or install from package manager, if applicable) +- Set python version (`uv venv --python 3.10`) (or whatever version you would like, as long as it's 3.10+. Careful if you use pyenv.) diff --git a/docs/index.md b/docs/index.md index c51751d..abe961b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,6 +5,7 @@ end-before: ``` [license]: license +[serializing]: packer_and_unpacker ```{toctree} --- @@ -14,6 +15,8 @@ maxdepth: 1 yaml-config reference +serialization +development License Changelog ``` diff --git a/docs/reference.md b/docs/reference.md index aedf5ec..9dfbe00 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1,45 +1,115 @@ # Reference -## Network - -```{eval-rst} -.. automodule:: heisskleber.mqtt - :members: -.. automodule:: heisskleber.zmq - :members: -.. automodule:: heisskleber.serial - :members: -.. automodule:: heisskleber.udp - :members: -``` - ## Baseclasses ```{eval-rst} -.. automodule:: heisskleber.core.types +.. autoclass:: heisskleber.core.AsyncSink + :members: + +.. autoclass:: heisskleber.core.AsyncSource :members: ``` -## Stream +## Serialization -Work on streaming data. +See for a tutorial on how to implement custom packer and unpacker for (de-)serialization. ```{eval-rst} -.. automodule:: heisskleber.stream.filter - :members: __aiter__ +.. autoclass:: heisskleber.core::Packer -.. automodule:: heisskleber.stream.butter - :members: +.. autoclass:: heisskleber.core::Unpacker -.. automodule:: heisskleber.stream.gh-filter - :members: +.. autoclass:: heisskleber.core.unpacker::JSONUnpacker + +.. autoclass:: heisskleber.core.packer::JSONPacker ``` -## Config - -### Loading configs +### Errors ```{eval-rst} -.. automodule:: heisskleber.config - :members: +.. autoclass:: heisskleber.core::UnpackError + +.. autoclass:: heisskleber.core::PackerError +``` + +## Implementations (Adapters) + +### MQTT + +```{eval-rst} +.. automodule:: heisskleber.mqtt + :no-members: + +.. autoclass:: heisskleber.mqtt.MqttSink + :members: send + +.. autoclass:: heisskleber.mqtt.MqttSource + :members: receive, subscribe + +.. autoclass:: heisskleber.mqtt.MqttConf + :members: +``` + +### ZMQ + +```{eval-rst} +.. autoclass:: heisskleber.zmq::ZmqConf +``` + +```{eval-rst} +.. autoclass:: heisskleber.zmq::ZmqSink + :members: send +``` + +```{eval-rst} +.. autoclass:: heisskleber.zmq::ZmqSource + :members: receive +``` + +### Serial + +```{eval-rst} +.. autoclass:: heisskleber.serial::SerialConf +``` + +```{eval-rst} +.. autoclass:: heisskleber.serial::SerialSink + :members: send +``` + +```{eval-rst} +.. autoclass:: heisskleber.serial::SerialSource + :members: receive +``` + +### TCP + +```{eval-rst} +.. autoclass:: heisskleber.tcp::TcpConf +``` + +```{eval-rst} +.. autoclass:: heisskleber.tcp::TcpSink + :members: send +``` + +```{eval-rst} +.. autoclass:: heisskleber.tcp::TcpSource + :members: receive +``` + +### UDP + +```{eval-rst} +.. autoclass:: heisskleber.udp::UdpConf +``` + +```{eval-rst} +.. autoclass:: heisskleber.udp::UdpSink + :members: send +``` + +```{eval-rst} +.. autoclass:: heisskleber.udp::UdpSource + :members: receive ``` diff --git a/docs/requirements.txt b/docs/requirements.txt index 1e78a63..30fcdcd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,3 @@ -furo==2024.1.29 -sphinx==7.2.6 -myst_parser==2.0.0 +furo==2024.8.6 +sphinx==8.1.3 +myst_parser==4.0.0 diff --git a/docs/serialization.md b/docs/serialization.md new file mode 100644 index 0000000..d5e06a2 --- /dev/null +++ b/docs/serialization.md @@ -0,0 +1,72 @@ +# Serialization + +## Implementing a custom Packer + +The packer class is defined in heisskleber.core.packer.py as a Protocol [see PEP 544](https://peps.python.org/pep-0544/). + +```python + T = TypeVar("T", contravariant=True) + + class Packer(Protocol[T]): + def __call__(self, data: T) -> bytes: + pass +``` + +Users can create custom Packer classes with variable input data, either as callable classes, subclasses of the packer class or functions. +Please note, that to satisfy type checking engines, the argument must be named `data`, but being Python, it's obviously not enforced at runtime. +The AsyncSink's type is defined by the concrete packer implementation. So if your Packer packs strings to bytes, the AsyncSink will be of type `AsyncSink[str]`, +indicating that the send function takes strings only, see example below: + +```python + from heisskleber import MqttSink, MqttConf + + def string_packer(data: str) -> bytes: + return data.encode("ascii") + + async def main(): + sink = MqttSink(MqttConf(), packer = string_packer) + await sink.send("Hi there!") # This is fine + await sink.send({"data": 3.14}) # Type checker will complain +``` + +Heisskleber comes with default packers, such as the JSON_Packer, which can be importet as json_packer from heisskleber.core and is the default value for most Sinks. + +## Implementing a custom Unpacker + +The unpacker's responsibility is creating usable data from serialized byte strings. +This may be a serialized json string which is unpacked into a dictionary, but could be anything the user defines. +In heisskleber.core.unpacker.py the Unpacker Protocol is defined. + +```python + class Unpacker(Protocol[T]): + def __call__(self, payload: bytes) -> tuple[T, dict[str, Any]]: + pass +``` + +Here, the payload is fixed to be of type bytes and the return type is a combination of a user-defined data type and a dictionary of meta-data. + +```{eval-rst} +.. note:: +Please Note: The extra dictionary may be updated by the Source, e.g. the MqttSource will add a "topic" field, received from the mqtt node. +``` + +The receive function of an AsyncSource object will have its return type informed by the signature of the unpacker. + +```python + from heisskleber import MqttSource, MqttConf + import time + + def csv_unpacker(payload: bytes) -> tuple[list[str], dict[str, Any]]: + # Unpack a utf-8 encoded csv string, such as b'1,42,3.14,100.0' to [1.0, 42.0, 3.14, 100.0] + # Adds some exemplary meta data + return [float(chunk) for chunk in payload.decode().split(",")], {"processed_at": time.time()} + + async def main(): + sub = MqttSource(MqttConf, unpacker = csv_unpacker) + data, extra = await sub.receive() + assert isinstance(data, list[str]) # passes +``` + +## Error handling + +To be implemented... diff --git a/docs/yaml-config.md b/docs/yaml-config.md index 8e3d7d1..6f129c9 100644 --- a/docs/yaml-config.md +++ b/docs/yaml-config.md @@ -11,10 +11,6 @@ The configuration parameters are host, port, ssl, user and password to establish - 2: "At least once", where messages are assured to arrive but duplicates can occur. - 3: "Exactly once", where messages are assured to arrive exactly once. - **max_saved_messages**: maximum number of messages that will be saved in the buffer until connection is available. -- **packstyle**: key of the serialization technique to use. Currently only JSON is supported. -- **source_id**: id of the device that will be used to identify the MQTT messages to be used by clients to format the topic. - Suggested topic format is in the form of `f"/{measurement_type}/{source_id}"`, eg. "/temperature/box-01". -- **topics**: the topics that the mqtt forwarder will subscribe to. ```yaml # Heisskleber config file for MqttConf @@ -27,10 +23,4 @@ qos: 0 # quality of service, 0=at most once, 1=at least once, 2=exactly once timeout_s: 60 retain: false # save last message max_saved_messages: 100 # buffer messages in until connection available -packstyle: json - -# configs only valid for mqtt forwarder -mapping: /deprecated/ -source_id: box-01 -topics: ["topic1", "topic2"] ``` diff --git a/heisskleber/__init__.py b/heisskleber/__init__.py deleted file mode 100644 index b485226..0000000 --- a/heisskleber/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Heisskleber.""" - -from .core.async_factories import get_async_sink, get_async_source -from .core.factories import get_publisher, get_sink, get_source, get_subscriber -from .core.types import AsyncSink, AsyncSource, Sink, Source - -__all__ = [ - "get_source", - "get_sink", - "get_publisher", - "get_subscriber", - "get_async_source", - "get_async_sink", - "Sink", - "Source", - "AsyncSink", - "AsyncSource", -] -__version__ = "0.5.7" diff --git a/heisskleber/broker/__init__.py b/heisskleber/broker/__init__.py deleted file mode 100644 index df85526..0000000 --- a/heisskleber/broker/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .zmq_broker import zmq_broker as start_zmq_broker - -__all__ = ["start_zmq_broker"] diff --git a/heisskleber/broker/zmq_broker.py b/heisskleber/broker/zmq_broker.py deleted file mode 100644 index f4f21f9..0000000 --- a/heisskleber/broker/zmq_broker.py +++ /dev/null @@ -1,60 +0,0 @@ -import sys - -import zmq -from zmq import Socket - -from heisskleber.config import load_config -from heisskleber.zmq.config import ZmqConf as BrokerConf - - -class BrokerBindingError(Exception): - pass - - -def bind_socket(socket: Socket, address: str, socket_type: str, verbose=False) -> None: - """Bind a ZMQ socket and handle errors.""" - if verbose: - print(f"creating {socket_type} socket") - try: - socket.bind(address) - except Exception as err: - error_message = f"failed to bind to {socket_type}: {err}" - raise BrokerBindingError(error_message) from err - if verbose: - print(f"successfully bound to {socket_type} socket: {address}") - - -def create_proxy(xpub: Socket, xsub: Socket, verbose=False) -> None: - """Create a ZMQ proxy to connect XPUB and XSUB sockets.""" - if verbose: - print("creating proxy") - try: - zmq.proxy(xpub, xsub) - except Exception as err: - error_message = f"failed to create proxy: {err}" - raise BrokerBindingError(error_message) from err - - -# TODO reimplement as object? -def zmq_broker(config: BrokerConf) -> None: - """Start a zmq broker. - - Binds to a publisher and subscriber port, allowing many to many connections.""" - ctx = zmq.Context() - - xpub = ctx.socket(zmq.XPUB) - xsub = ctx.socket(zmq.XSUB) - - try: - bind_socket(xpub, config.subscriber_address, "publisher", config.verbose) - bind_socket(xsub, config.publisher_address, "subscriber", config.verbose) - create_proxy(xpub, xsub, config.verbose) - except BrokerBindingError as e: - print(e) - sys.exit(-1) - - -def main() -> None: - """Start a zmq broker, with a user specified configuration.""" - broker_config = load_config(BrokerConf(), "zmq") - zmq_broker(broker_config) diff --git a/heisskleber/config/__init__.py b/heisskleber/config/__init__.py deleted file mode 100644 index d9e9a7e..0000000 --- a/heisskleber/config/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .config import BaseConf, Config -from .parse import load_config - -__all__ = ["load_config", "BaseConf", "Config"] diff --git a/heisskleber/config/cmdline.py b/heisskleber/config/cmdline.py deleted file mode 100644 index b71588c..0000000 --- a/heisskleber/config/cmdline.py +++ /dev/null @@ -1,47 +0,0 @@ -import argparse - - -class KeyValue(argparse.Action): - def __call__(self, parser, args, values, option_string=None) -> None: - try: - params = dict(x.split("=") for x in values) - except ValueError as ex: - raise argparse.ArgumentError( - self, - f'Could not parse argument "{values}" as k1=v1 k2=v2 ... format: {ex}', - ) from ex - setattr(args, self.dest, params) - - -def get_cmdline(args=None) -> dict: - """ - get commandline arguments and return a dictionary of - the provided arguments. - - available commandline arguments are: - --verbose: flag to toggle debugging output - --print-stdout: flag to toggle all data printed to stdout - --param key1=value1 key2=value2: allows to pass service specific - parameters - """ - arp = argparse.ArgumentParser() - arp.add_argument("--verbose", action="store_true", help="debug output flag") - arp.add_argument( - "--print-stdout", - action="store_true", - help="toggles output of all data to stdout", - ) - arp.add_argument( - "--params", - nargs="*", - action=KeyValue, - ) - args = arp.parse_args(args) - config = {} - if args.verbose: - config["verbose"] = args.verbose - if args.print_stdout: - config["print_stdout"] = args.print_stdout - if args.params: - config |= args.params - return config diff --git a/heisskleber/config/config.py b/heisskleber/config/config.py deleted file mode 100644 index 1fab37c..0000000 --- a/heisskleber/config/config.py +++ /dev/null @@ -1,35 +0,0 @@ -import socket -import warnings -from dataclasses import dataclass -from typing import Any, TypeVar - - -@dataclass -class BaseConf: - """ - default configuration class for generic configuration info - """ - - verbose: bool = False - print_stdout: bool = False - - def __setitem__(self, key: str, value: Any) -> None: - if hasattr(self, key): - self.__setattr__(key, value) - else: - warnings.warn(UserWarning(f"no such class member: {key}"), stacklevel=2) - - def __getitem__(self, key: str) -> Any: - if hasattr(self, key): - return getattr(self, key) - else: - warnings.warn(UserWarning(f"no such class member: {key}"), stacklevel=2) - - @property - def serial_number(self) -> str: - return socket.gethostname().upper() - - -Config = TypeVar( - "Config", bound=BaseConf -) # https://stackoverflow.com/a/46227137 , https://docs.python.org/3/library/typing.html#typing.TypeVar diff --git a/heisskleber/config/parse.py b/heisskleber/config/parse.py deleted file mode 100644 index d73fef7..0000000 --- a/heisskleber/config/parse.py +++ /dev/null @@ -1,75 +0,0 @@ -import logging -from pathlib import Path -from typing import Any - -import yaml - -from heisskleber.config.cmdline import get_cmdline -from heisskleber.config.config import Config - -log = logging.getLogger(__name__) - - -def get_config_dir() -> Path: - config_dir = Path.home() / ".config" / "heisskleber" - if not config_dir.is_dir(): - log.error(f"no such directory: {config_dir}", stacklevel=2) - raise FileNotFoundError - return config_dir - - -def get_config_filepath(filename: str) -> Path: - config_filepath = get_config_dir() / filename - if not config_filepath.is_file(): - log.error(f"no such file: {config_filepath}", stacklevel=2) - raise FileNotFoundError - return config_filepath - - -def read_yaml_config_file(config_fpath: Path) -> dict[str, Any]: - with config_fpath.open() as config_filehandle: - return yaml.safe_load(config_filehandle) # type: ignore [no-any-return] - - -def update_config(config: Config, config_dict: dict[str, Any]) -> Config: - for config_key, config_value in config_dict.items(): - if not hasattr(config, config_key): - error_msg = f"no such configuration parameter: {config_key}, skipping" - log.info(error_msg, stacklevel=2) - continue - cast_func = type(config[config_key]) - try: - config[config_key] = cast_func(config_value) - except Exception as e: - log.warning( - f"failed to cast {config_value} to {type(config[config_key])}: {e}. skipping", - stacklevel=2, - ) - continue - return config - - -def load_config(config: Config, config_filename: str, read_commandline: bool = True) -> Config: - """Load the config file and update the config object. - - Parameters - ---------- - config : BaseConf - The config object to fill with values. - config_filename : str - The name of the config file in $HOME/.config - If the file does not have an extension the default extension .yaml is appended. - read_commandline : bool - Whether to read arguments from the command line. Optional. Defaults to True. - """ - config_filename = config_filename if "." in config_filename else config_filename + ".yaml" - config_filepath = get_config_filepath(config_filename) - config_dict = read_yaml_config_file(config_filepath) - config = update_config(config, config_dict) - - if not read_commandline: - return config - - config_dict = get_cmdline() - config = update_config(config, config_dict) - return config diff --git a/heisskleber/console/sink.py b/heisskleber/console/sink.py deleted file mode 100644 index c6d88f7..0000000 --- a/heisskleber/console/sink.py +++ /dev/null @@ -1,55 +0,0 @@ -import json -import time - -from heisskleber.core.types import AsyncSink, Serializable, Sink - - -class ConsoleSink(Sink): - def __init__(self, pretty: bool = False, verbose: bool = False) -> None: - self.verbose = verbose - self.pretty = pretty - - def send(self, data: dict[str, Serializable], topic: str) -> None: - verbose_topic = topic + ":\t" if self.verbose else "" - if self.pretty: - print(verbose_topic + json.dumps(data, indent=4)) - else: - print(verbose_topic + str(data)) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(pretty={self.pretty}, verbose={self.verbose})" - - def start(self) -> None: - pass - - def stop(self) -> None: - pass - - -class AsyncConsoleSink(AsyncSink): - def __init__(self, pretty: bool = False, verbose: bool = False) -> None: - self.verbose = verbose - self.pretty = pretty - - async def send(self, data: dict[str, Serializable], topic: str) -> None: - verbose_topic = topic + ":\t" if self.verbose else "" - if self.pretty: - print(verbose_topic + json.dumps(data, indent=4)) - else: - print(verbose_topic + str(data)) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(pretty={self.pretty}, verbose={self.verbose})" - - def start(self) -> None: - pass - - def stop(self) -> None: - pass - - -if __name__ == "__main__": - sink = ConsoleSink() - while True: - sink.send({"test": "test"}, "test") - time.sleep(1) diff --git a/heisskleber/console/source.py b/heisskleber/console/source.py deleted file mode 100644 index ae3910a..0000000 --- a/heisskleber/console/source.py +++ /dev/null @@ -1,98 +0,0 @@ -import asyncio -import json -import sys -import time -from queue import SimpleQueue -from threading import Thread - -from heisskleber.core.types import AsyncSource, Serializable, Source - - -class ConsoleSource(Source): - def __init__(self, topic: str = "console") -> None: - self.topic = topic - self.queue = SimpleQueue() - self.pack = json.loads - self.thread: Thread | None = None - - def listener_task(self): - while True: - try: - data = sys.stdin.readline() - payload = self.pack(data) - self.queue.put(payload) - except json.decoder.JSONDecodeError: - print("Invalid JSON") - continue - except ValueError: - break - print("listener task finished") - - def receive(self) -> tuple[str, dict[str, Serializable]]: - if not self.thread: - self.start() - - data = self.queue.get() - return self.topic, data - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(topic={self.topic})" - - def start(self) -> None: - self.thread = Thread(target=self.listener_task, daemon=True) - self.thread.start() - - def stop(self) -> None: - if self.thread: - sys.stdin.close() - self.thread.join() - - -class AsyncConsoleSource(AsyncSource): - def __init__(self, topic: str = "console") -> None: - self.topic = topic - self.queue: asyncio.Queue[dict[str, Serializable]] = asyncio.Queue(maxsize=10) - self.pack = json.loads - self.task: asyncio.Task[None] | None = None - - async def listener_task(self): - while True: - data = sys.stdin.readline() - payload = self.pack(data) - await self.queue.put(payload) - - async def receive(self) -> tuple[str, dict[str, Serializable]]: - if not self.task: - self.start() - - data = await self.queue.get() - return self.topic, data - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(topic={self.topic})" - - def start(self) -> None: - self.task = asyncio.create_task(self.listener_task()) - - def stop(self) -> None: - if self.task: - self.task.cancel() - - -if __name__ == "__main__": - console_source = ConsoleSource() - console_source.start() - - print("Listening to console input.") - - count = 0 - - try: - while True: - print(console_source.receive()) - time.sleep(1) - count += 1 - print(count) - except KeyboardInterrupt: - print("Stopped") - sys.exit(0) diff --git a/heisskleber/core/async_factories.py b/heisskleber/core/async_factories.py deleted file mode 100644 index e9bdb36..0000000 --- a/heisskleber/core/async_factories.py +++ /dev/null @@ -1,59 +0,0 @@ -from heisskleber.config import BaseConf, load_config -from heisskleber.mqtt import AsyncMqttPublisher, AsyncMqttSubscriber, MqttConf -from heisskleber.udp import AsyncUdpSink, AsyncUdpSource, UdpConf -from heisskleber.zmq import ZmqAsyncPublisher, ZmqAsyncSubscriber, ZmqConf - -from .types import AsyncSink, AsyncSource - -_registered_async_sinks: dict[str, tuple[type[AsyncSink], type[BaseConf]]] = { - "mqtt": (AsyncMqttPublisher, MqttConf), - "zmq": (ZmqAsyncPublisher, ZmqConf), - "udp": (AsyncUdpSink, UdpConf), -} - -_registered_async_sources: dict[str, tuple] = { - "mqtt": (AsyncMqttSubscriber, MqttConf), - "zmq": (ZmqAsyncSubscriber, ZmqConf), - "udp": (AsyncUdpSource, UdpConf), -} - - -def get_async_sink(name: str) -> AsyncSink: - """ - Factory function to create a sink object. - - Parameters: - name: Name of the sink to create. - config: Configuration object to use for the sink. - """ - - if name not in _registered_async_sinks: - error_message = f"{name} is not a registered Sink." - raise KeyError(error_message) - - pub_cls, conf_cls = _registered_async_sinks[name] - - config = load_config(conf_cls(), name, read_commandline=False) - - return pub_cls(config) - - -def get_async_source(name: str, topic: str | list[str] | tuple[str]) -> AsyncSource: - """ - Factory function to create a source object. - - Parameters: - name: Name of the source to create. - config: Configuration object to use for the source. - topic: Topic to subscribe to. - """ - - if name not in _registered_async_sources: - error_message = f"{name} is not a registered Source." - raise KeyError(error_message) - - sub_cls, conf_cls = _registered_async_sources[name] - - config = load_config(conf_cls(), name, read_commandline=False) - - return sub_cls(config, topic) diff --git a/heisskleber/core/factories.py b/heisskleber/core/factories.py deleted file mode 100644 index 99ca39f..0000000 --- a/heisskleber/core/factories.py +++ /dev/null @@ -1,90 +0,0 @@ -from heisskleber.config import BaseConf, load_config -from heisskleber.core.types import Sink, Source -from heisskleber.mqtt import MqttConf, MqttPublisher, MqttSubscriber -from heisskleber.serial import SerialConf, SerialPublisher, SerialSubscriber -from heisskleber.udp import UdpConf, UdpPublisher, UdpSubscriber -from heisskleber.zmq import ZmqConf, ZmqPublisher, ZmqSubscriber - -_registered_sinks: dict[str, tuple[type[Sink], type[BaseConf]]] = { - "zmq": (ZmqPublisher, ZmqConf), - "mqtt": (MqttPublisher, MqttConf), - "serial": (SerialPublisher, SerialConf), - "udp": (UdpPublisher, UdpConf), -} - -_registered_sources: dict[str, tuple[type[Source], type[BaseConf]]] = { - "zmq": (ZmqSubscriber, ZmqConf), - "mqtt": (MqttSubscriber, MqttConf), - "serial": (SerialSubscriber, SerialConf), - "udp": (UdpSubscriber, UdpConf), -} - - -def get_sink(name: str) -> Sink: - """ - Factory function to create a sink object. - - Parameters: - name: Name of the sink to create. - config: Configuration object to use for the sink. - """ - - if name not in _registered_sinks: - error_message = f"{name} is not a registered Sink." - raise KeyError(error_message) - - pub_cls, conf_cls = _registered_sinks[name] - - print(f"loading {name} config") - config = load_config(conf_cls(), name, read_commandline=False) - - return pub_cls(config) - - -def get_source(name: str, topic: str | list[str]) -> Source: - """ - Factory function to create a source object. - - Parameters: - name: Name of the source to create. - config: Configuration object to use for the source. - topic: Topic to subscribe to. - """ - - if name not in _registered_sinks: - error_message = f"{name} is not a registered Source." - raise KeyError(error_message) - - sub_cls, conf_cls = _registered_sources[name] - - print(f"loading {name} config") - config = load_config(conf_cls(), name, read_commandline=False) - - return sub_cls(config, topic) - - -def get_subscriber(name: str, topic: str | list[str]) -> Source: - """ - Deprecated: Factory function to create a source object (formerly known as subscriber). - - Parameters: - name: Name of the source to create. - config: Configuration object to use for the source. - topic: Topic to subscribe to. - """ - - print("Deprecated: use get_source instead.") - return get_source(name, topic) - - -def get_publisher(name: str) -> Sink: - """ - Deprecated: Factory function to create a sink object (formerly known as publisher). - - Parameters: - name: Name of the sink to create. - config: Configuration object to use for the sink. - """ - - print("Deprecated: use get_sink instead.") - return get_sink(name) diff --git a/heisskleber/core/packer.py b/heisskleber/core/packer.py deleted file mode 100644 index fcf5a73..0000000 --- a/heisskleber/core/packer.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Packer and unpacker for network data.""" -import json -import pickle -from typing import Any, Callable - -from .types import Serializable - - -def get_packer(style: str) -> Callable[[dict[str, Serializable]], str]: - """Return a packer function for the given style. - - Packer func serializes a given dict.""" - if style in _packstyles: - return _packstyles[style] - else: - return _packstyles["default"] - - -def get_unpacker(style: str) -> Callable[[str], dict[str, Serializable]]: - """Return an unpacker function for the given style. - - Unpacker func deserializes a string.""" - if style in _unpackstyles: - return _unpackstyles[style] - else: - return _unpackstyles["default"] - - -def serialpacker(data: dict[str, Any]) -> str: - return ",".join([str(v) for v in data.values()]) - - -_packstyles: dict[str, Callable[[dict[str, Serializable]], str]] = { - "default": json.dumps, - "json": json.dumps, - "pickle": pickle.dumps, # type: ignore - "serial": serialpacker, -} - -_unpackstyles: dict[str, Callable[[str], dict[str, Serializable]]] = { - "default": json.loads, - "json": json.loads, - "pickle": pickle.loads, # type: ignore -} diff --git a/heisskleber/core/types.py b/heisskleber/core/types.py deleted file mode 100644 index a045421..0000000 --- a/heisskleber/core/types.py +++ /dev/null @@ -1,180 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import AsyncGenerator, Generator -from typing import Any, Callable, Union - -from heisskleber.config import BaseConf - -Serializable = Union[int, float] - - -class Sink(ABC): - """ - Sink interface to send() data to. - """ - - pack: Callable[[dict[str, Serializable]], str] - - @abstractmethod - def __init__(self, config: BaseConf) -> None: - """ - Initialize the publisher with a configuration object. - """ - pass - - @abstractmethod - def send(self, data: dict[str, Serializable], topic: str) -> None: - """ - Send data via the implemented output stream. - """ - pass - - @abstractmethod - def __repr__(self) -> str: - pass - - @abstractmethod - def start(self) -> None: - """ - Start any background processes and tasks. - """ - pass - - @abstractmethod - def stop(self) -> None: - """ - Stop any background processes and tasks. - """ - pass - - -class Source(ABC): - """ - Source interface that emits data via the receive() method. - """ - - unpack: Callable[[str], dict[str, Serializable]] - - def __iter__(self) -> Generator[tuple[str, dict[str, Serializable]], None, None]: - topic, data = self.receive() - yield topic, data - - @abstractmethod - def __init__(self, config: BaseConf, topic: str | list[str]) -> None: - """ - Initialize the subscriber with a topic and a configuration object. - """ - pass - - @abstractmethod - def receive(self) -> tuple[str, dict[str, Serializable]]: - """ - Blocking function to receive data from the implemented input stream. - - Data is returned as a tuple of (topic, data). - """ - pass - - @abstractmethod - def __repr__(self) -> str: - pass - - @abstractmethod - def start(self) -> None: - """ - Start any background processes and tasks. - """ - pass - - @abstractmethod - def stop(self) -> None: - """ - Stop any background processes and tasks. - """ - pass - - -class AsyncSource(ABC): - """ - AsyncSubscriber interface - """ - - async def __aiter__(self) -> AsyncGenerator[tuple[str, dict[str, Serializable]], None]: - while True: - topic, data = await self.receive() - yield topic, data - - @abstractmethod - def __init__(self, config: Any, topic: str | list[str]) -> None: - """ - Initialize the subscriber with a topic and a configuration object. - """ - pass - - @abstractmethod - async def receive(self) -> tuple[str, dict[str, Serializable]]: - """ - Blocking function to receive data from the implemented input stream. - - Data is returned as a tuple of (topic, data). - """ - pass - - @abstractmethod - def __repr__(self) -> str: - pass - - @abstractmethod - def start(self) -> None: - """ - Start any background processes and tasks. - """ - pass - - @abstractmethod - def stop(self) -> None: - """ - Stop any background processes and tasks. - """ - pass - - -class AsyncSink(ABC): - """ - Sink interface to send() data to. - """ - - pack: Callable[[dict[str, Serializable]], str] - - @abstractmethod - def __init__(self, config: BaseConf) -> None: - """ - Initialize the publisher with a configuration object. - """ - pass - - @abstractmethod - async def send(self, data: dict[str, Any], topic: str) -> None: - """ - Send data via the implemented output stream. - """ - pass - - @abstractmethod - def __repr__(self) -> str: - pass - - @abstractmethod - def start(self) -> None: - """ - Start any background processes and tasks. - """ - pass - - @abstractmethod - def stop(self) -> None: - """ - Stop any background processes and tasks. - """ - pass diff --git a/heisskleber/influxdb/config.py b/heisskleber/influxdb/config.py deleted file mode 100644 index 62251a1..0000000 --- a/heisskleber/influxdb/config.py +++ /dev/null @@ -1,20 +0,0 @@ -from dataclasses import dataclass - -from heisskleber.config import BaseConf - - -@dataclass -class InfluxDBConf(BaseConf): - host: str = "localhost" - port: int = 8086 - bucket: str = "test" - org: str = "test" - ssl: bool = False - read_token: str = "" - write_token: str = "" - all_access_token: str = "" - - @property - def url(self) -> str: - protocol = "https" if self.ssl else "http" - return f"{protocol}://{self.host}:{self.port}" diff --git a/heisskleber/influxdb/subscriber.py b/heisskleber/influxdb/subscriber.py deleted file mode 100644 index 2d4597f..0000000 --- a/heisskleber/influxdb/subscriber.py +++ /dev/null @@ -1,75 +0,0 @@ -import pandas as pd -from influxdb_client import InfluxDBClient - -from heisskleber.core.types import Source - -from .config import InfluxDBConf - - -def build_query(options: dict) -> str: - query = ( - f'from(bucket:"{options["bucket"]}")' - + f'|> range(start: {options["start"].isoformat("T")}, stop: {options["end"].isoformat("T")})' - + f'|> filter(fn:(r) => r._measurement == "{options["measurement"]}")' - ) - if options["filter"]: - for attribute, value in options["filter"].items(): - if isinstance(value, list): - query += f'|> filter(fn:(r) => r.{attribute} == "{value[0]}"' - for vv in value[1:]: - query += f' or r.{attribute} == "{vv}"' - query += ")" - else: - query += f'|> filter(fn:(r) => r.{attribute} == "{value}")' - - query += ( - f'|> aggregateWindow(every: {options["resample"]}, fn: mean)' - + '|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")' - ) - - return query - - -class Influx_Subscriber(Source): - def __init__(self, config: InfluxDBConf, query: str): - self.config = config - self.query = query - - self.client: InfluxDBClient = InfluxDBClient( - url=self.config.url, - token=self.config.all_access_token or self.config.read_token, - org=self.config.org, - timeout=60_000, - ) - self.reader = self.client.query_api() - - self._run_query() - self.index = 0 - - def receive(self) -> tuple[str, dict]: - row = self.df.iloc[self.index].to_dict() - self.index += 1 - return "influx", row - - def _run_query(self): - self.df: pd.DataFrame = self.reader.query_data_frame(self.query, org=self.config.org) - self.df["epoch"] = pd.to_numeric(self.df["_time"]) / 1e9 - self.df.drop( - columns=[ - "result", - "table", - "_start", - "_stop", - "_measurement", - "_time", - "topic", - ], - inplace=True, - ) - - def __iter__(self): - for _, row in self.df.iterrows(): - yield "influx", row.to_dict() - - def __next__(self): - return self.__iter__().__next__() diff --git a/heisskleber/influxdb/writer.py b/heisskleber/influxdb/writer.py deleted file mode 100644 index de67556..0000000 --- a/heisskleber/influxdb/writer.py +++ /dev/null @@ -1,48 +0,0 @@ -from influxdb_client import InfluxDBClient, WriteOptions - -from config import InfluxDBConf -from heisskleber.config import load_config - - -class Influx_Writer: - def __init__(self, config: InfluxDBConf): - self.config = config - # self.write_options = SYNCHRONOUS - self.write_options = WriteOptions( - batch_size=500, - flush_interval=10_000, - jitter_interval=2_000, - retry_interval=5_000, - max_retries=5, - max_retry_delay=30_000, - exponential_base=2, - ) - self.client = InfluxDBClient(url=self.config.url, token=self.config.token, org=self.config.org) - self.writer = self.client.write_api( - write_options=self.write_options, - ) - - def __del__(self): - self.writer.close() - self.client.close() - - def write_line(self, line): - self.writer.write(bucket=self.config.bucket, record=line) - - def write_from_generator(self, generator): - for line in generator: - self.writer.write(bucket=self.config.bucket, record=line) - - def write_from_line_generator(self, generator): - with InfluxDBClient( - url=self.config.url, token=self.config.token, org=self.config.org - ) as client, client.write_api( - write_options=self.write_options, - ) as write_api: - for line in generator: - write_api.write(bucket=self.config.bucket, record=line) - - -def get_parsed_flux_writer(): - config = load_config(InfluxDBConf(), "flux", read_commandline=False) - return Influx_Writer(config) diff --git a/heisskleber/mqtt/__init__.py b/heisskleber/mqtt/__init__.py deleted file mode 100644 index 4efa290..0000000 --- a/heisskleber/mqtt/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .config import MqttConf -from .publisher import MqttPublisher -from .publisher_async import AsyncMqttPublisher -from .subscriber import MqttSubscriber -from .subscriber_async import AsyncMqttSubscriber - -__all__ = ["MqttConf", "MqttPublisher", "MqttSubscriber", "AsyncMqttSubscriber", "AsyncMqttPublisher"] diff --git a/heisskleber/mqtt/config.py b/heisskleber/mqtt/config.py deleted file mode 100644 index bd82dd3..0000000 --- a/heisskleber/mqtt/config.py +++ /dev/null @@ -1,24 +0,0 @@ -from dataclasses import dataclass, field - -from heisskleber.config import BaseConf - - -@dataclass -class MqttConf(BaseConf): - """ - MQTT configuration class. - """ - - host: str = "localhost" - user: str = "" - password: str = "" - port: int = 1883 - ssl: bool = False - qos: int = 0 - retain: bool = False - topics: list[str] = field(default_factory=list) - mapping: str = "/deprecated/" # deprecated - packstyle: str = "json" - max_saved_messages: int = 100 - timeout_s: int = 60 - source_id: str = "box-01" diff --git a/heisskleber/mqtt/forwarder.py b/heisskleber/mqtt/forwarder.py deleted file mode 100644 index 370afd3..0000000 --- a/heisskleber/mqtt/forwarder.py +++ /dev/null @@ -1,23 +0,0 @@ -from heisskleber import get_publisher, get_subscriber -from heisskleber.config import load_config - -from .config import MqttConf - - -def map_topic(zmq_topic: str, mapping: str) -> str: - return mapping + zmq_topic - - -def main() -> None: - config: MqttConf = load_config(MqttConf(), "mqtt") - sub = get_subscriber("zmq", config.topics) - pub = get_publisher("mqtt") - - pub.pack = lambda x: x # type: ignore - sub.unpack = lambda x: x # type: ignore - - while True: - (zmq_topic, data) = sub.receive() - mqtt_topic = map_topic(zmq_topic, config.mapping) - - pub.send(data, mqtt_topic) diff --git a/heisskleber/mqtt/mqtt_base.py b/heisskleber/mqtt/mqtt_base.py deleted file mode 100644 index f5853e2..0000000 --- a/heisskleber/mqtt/mqtt_base.py +++ /dev/null @@ -1,97 +0,0 @@ -import ssl -import sys -import threading - -from paho.mqtt.client import Client as mqtt_client - -from .config import MqttConf - - -class ThreadDiedError(RuntimeError): - pass - - -_thread_died = threading.Event() - -_default_excepthook = threading.excepthook - - -def _set_thread_died_excepthook(args, /): - _default_excepthook(args) - global _thread_died - _thread_died.set() - - -threading.excepthook = _set_thread_died_excepthook - - -class MqttBase: - """ - Wrapper around eclipse paho mqtt client. - Handles connection and callbacks. - Callbacks may be overwritten in subclasses. - """ - - def __init__(self, config: MqttConf) -> None: - self.config = config - self.client = mqtt_client() - self.is_connected = False - - def start(self) -> None: - if not self.is_connected: - self.connect() - - def stop(self) -> None: - if self.client: - self.client.loop_stop() - self.is_connected = False - - def connect(self) -> None: - self.client.username_pw_set(self.config.user, self.config.password) - - # Add callbacks - self.client.on_connect = self._on_connect - self.client.on_disconnect = self._on_disconnect - self.client.on_publish = self._on_publish - self.client.on_message = self._on_message - - if self.config.ssl: - # By default, on Python 2.7.9+ or 3.4+, - # the default certification authority of the system is used. - self.client.tls_set(tls_version=ssl.PROTOCOL_TLS_CLIENT) - - self.client.connect(self.config.host, self.config.port) - self.client.loop_start() - self.is_connected = True - - @staticmethod - def _raise_if_thread_died() -> None: - global _thread_died - if _thread_died.is_set(): - raise ThreadDiedError() - - # MQTT callbacks - def _on_connect(self, client, userdata, flags, return_code) -> None: - if return_code == 0: - print(f"MQTT node connected to {self.config.host}:{self.config.port}") - else: - print("Connection failed!") - if self.config.verbose: - print(flags) - - def _on_disconnect(self, client, userdata, return_code) -> None: - print(f"Disconnected from broker with return code {return_code}") - if return_code != 0: - print("Killing this service") - sys.exit(-1) - - def _on_publish(self, client, userdata, message_id) -> None: - if self.config.verbose: - print(f"Published message with id {message_id}, qos={self.config.qos}") - - def _on_message(self, client, userdata, message) -> None: - if self.config.verbose: - print(f"Received message: {message.payload!s}, topic: {message.topic}, qos: {message.qos}") - - def __del__(self) -> None: - self.stop() diff --git a/heisskleber/mqtt/publisher.py b/heisskleber/mqtt/publisher.py deleted file mode 100644 index f5c6ea0..0000000 --- a/heisskleber/mqtt/publisher.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -from heisskleber.core.packer import get_packer -from heisskleber.core.types import Serializable, Sink - -from .config import MqttConf -from .mqtt_base import MqttBase - - -class MqttPublisher(MqttBase, Sink): - """ - MQTT publisher class. - Can be used everywhere that a flucto style publishing connection is required. - - Network message loop is handled in a separated thread. - """ - - def __init__(self, config: MqttConf) -> None: - super().__init__(config) - self.pack = get_packer(config.packstyle) - - def send(self, data: dict[str, Serializable], topic: str) -> None: - """ - Takes python dictionary, serializes it according to the packstyle - and sends it to the broker. - - Publishing is asynchronous - """ - if not self.is_connected: - self.start() - - self._raise_if_thread_died() - - payload = self.pack(data) - self.client.publish(topic, payload, qos=self.config.qos, retain=self.config.retain) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.host}, port={self.config.port})" - - def start(self) -> None: - super().start() - - def stop(self) -> None: - super().stop() diff --git a/heisskleber/mqtt/publisher_async.py b/heisskleber/mqtt/publisher_async.py deleted file mode 100644 index bc48e72..0000000 --- a/heisskleber/mqtt/publisher_async.py +++ /dev/null @@ -1,70 +0,0 @@ -from asyncio import Queue, Task, create_task, sleep - -import aiomqtt - -from heisskleber.core.packer import get_packer -from heisskleber.core.types import AsyncSink, Serializable - -from .config import MqttConf - - -class AsyncMqttPublisher(AsyncSink): - """ - MQTT publisher class. - Can be used everywhere that a flucto style publishing connection is required. - - Network message loop is handled in a separated thread. - """ - - def __init__(self, config: MqttConf) -> None: - self.config = config - self.pack = get_packer(config.packstyle) - self._send_queue: Queue[tuple[dict[str, Serializable], str]] = Queue() - self._sender_task: Task[None] | None = None - - async def send(self, data: dict[str, Serializable], topic: str) -> None: - """ - Takes python dictionary, serializes it according to the packstyle - and sends it to the broker. - - Publishing is asynchronous - """ - if not self._sender_task: - self.start() - - await self._send_queue.put((data, topic)) - - async def send_work(self) -> None: - """ - Takes python dictionary, serializes it according to the packstyle - and sends it to the broker. - - Publishing is asynchronous - """ - while True: - try: - async with aiomqtt.Client( - hostname=self.config.host, - port=self.config.port, - username=self.config.user, - password=self.config.password, - timeout=float(self.config.timeout_s), - ) as client: - while True: - data, topic = await self._send_queue.get() - payload = self.pack(data) - await client.publish(topic, payload) - except aiomqtt.MqttError: - print("Connection to MQTT broker failed. Retrying in 5 seconds") - await sleep(5) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(broker={self.config.host}, port={self.config.port})" - - def start(self) -> None: - self._sender_task = create_task(self.send_work()) - - def stop(self) -> None: - if self._sender_task: - self._sender_task.cancel() - self._sender_task = None diff --git a/heisskleber/mqtt/subscriber.py b/heisskleber/mqtt/subscriber.py deleted file mode 100644 index 2c0cacf..0000000 --- a/heisskleber/mqtt/subscriber.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import annotations - -from queue import SimpleQueue -from typing import Any - -from paho.mqtt.client import MQTTMessage - -from heisskleber.core.packer import get_unpacker -from heisskleber.core.types import Source - -from .config import MqttConf -from .mqtt_base import MqttBase - - -class MqttSubscriber(MqttBase, Source): - """ - MQTT subscriber, wraps around ecplipse's paho mqtt client. - Network message loop is handled in a separated thread. - - Incoming messages are saved as a stack when not processed via the receive() function. - """ - - def __init__(self, config: MqttConf, topics: str | list[str]) -> None: - super().__init__(config) - self.topics = topics - self._message_queue: SimpleQueue[MQTTMessage] = SimpleQueue() - self.unpack = get_unpacker(config.packstyle) - - def subscribe(self, topics: str | list[str] | tuple[str]) -> None: - """ - Subscribe to one or multiple topics - """ - if not self.is_connected: - super().start() - self.client.on_message = self._on_message - - if isinstance(topics, (list, tuple)): - # if subscribing to multiple topics, use a list of tuples - subscription_list = [(topic, self.config.qos) for topic in topics] - self.client.subscribe(subscription_list) - else: - self.client.subscribe(topics, self.config.qos) - if self.config.verbose: - print(f"Subscribed to: {topics}") - - def receive(self) -> tuple[str, dict[str, Any]]: - """ - Reads a message from mqtt and returns it - - Messages are saved in a stack, if no message is available, this function blocks. - - Returns: - tuple(topic: str, message: dict): the message received - """ - if not self.client: - self.start() - - self._raise_if_thread_died() - mqtt_message = self._message_queue.get(block=True, timeout=self.config.timeout_s) - - message_returned = self.unpack(mqtt_message.payload.decode()) - return (mqtt_message.topic, message_returned) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.host}, port={self.config.port})" - - def start(self) -> None: - super().start() - self.subscribe(self.topics) - self.client.on_message = self._on_message - - def stop(self) -> None: - super().stop() - - # callback to add incoming messages onto stack - def _on_message(self, client, userdata, message) -> None: - self._message_queue.put(message) - - if self.config.verbose: - print(f"Topic: {message.topic}") - print(f"MQTT message: {message.payload.decode()}") diff --git a/heisskleber/mqtt/subscriber_async.py b/heisskleber/mqtt/subscriber_async.py deleted file mode 100644 index 5bb8da5..0000000 --- a/heisskleber/mqtt/subscriber_async.py +++ /dev/null @@ -1,86 +0,0 @@ -from asyncio import Queue, Task, create_task, sleep - -from aiomqtt import Client, Message, MqttError - -from heisskleber.core.packer import get_unpacker -from heisskleber.core.types import AsyncSource, Serializable -from heisskleber.mqtt import MqttConf - - -class AsyncMqttSubscriber(AsyncSource): - """Asynchronous MQTT susbsciber based on aiomqtt. - - Data is received by the `receive` method returns the newest message in the queue. - """ - - def __init__(self, config: MqttConf, topic: str | list[str]) -> None: - self.config: MqttConf = config - self.client = Client( - hostname=self.config.host, - port=self.config.port, - username=self.config.user, - password=self.config.password, - ) - self.topics = topic - self.unpack = get_unpacker(self.config.packstyle) - self.message_queue: Queue[Message] = Queue(self.config.max_saved_messages) - self._listener_task: Task[None] | None = None - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(broker={self.config.host}, port={self.config.port})" - - def start(self) -> None: - self._listener_task = create_task(self.run()) - - def stop(self) -> None: - if self._listener_task: - self._listener_task.cancel() - self._listener_task = None - - async def receive(self) -> tuple[str, dict[str, Serializable]]: - """ - Await the newest message in the queue and return Tuple - """ - if not self._listener_task: - self.start() - mqtt_message = await self.message_queue.get() - return self._handle_message(mqtt_message) - - async def run(self): - """ - Handle the connection to MQTT broker and run the message loop. - """ - while True: - try: - async with self.client: - await self._subscribe_topics() - await self._listen_mqtt_loop() - except MqttError as e: - print(f"MqttError: {e}") - print("Connection to MQTT failed. Retrying...") - await sleep(1) - - async def _listen_mqtt_loop(self) -> None: - """ - Listen to incoming messages asynchronously and put them into a queue - """ - async with self.client.messages() as messages: - # async with self.client.filtered_messages(self.topics) as messages: - async for message in messages: - await self.message_queue.put(message) - - def _handle_message(self, message: Message) -> tuple[str, dict[str, Serializable]]: - if not isinstance(message.payload, bytes): - error_msg = "Payload is not of type bytes." - raise TypeError(error_msg) - - topic = str(message.topic) - message_returned = self.unpack(message.payload.decode()) - return (topic, message_returned) - - async def _subscribe_topics(self) -> None: - print(f"subscribing to {self.topics}") - if isinstance(self.topics, list): - await self.client.subscribe([(topic, self.config.qos) for topic in self.topics]) - else: - await self.client.subscribe(self.topics, self.config.qos) diff --git a/heisskleber/run/cli.py b/heisskleber/run/cli.py deleted file mode 100644 index 9306ce6..0000000 --- a/heisskleber/run/cli.py +++ /dev/null @@ -1,95 +0,0 @@ -import argparse -import sys -from typing import Callable, Union - -from heisskleber.config import load_config -from heisskleber.console.sink import ConsoleSink -from heisskleber.core.factories import _registered_sources -from heisskleber.mqtt import MqttSubscriber -from heisskleber.udp import UdpSubscriber -from heisskleber.zmq import ZmqSubscriber - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - prog="hkcli", - description="Heisskleber command line interface", - usage="%(prog)s [options]", - ) - parser.add_argument( - "-t", - "--type", - type=str, - choices=["zmq", "mqtt", "serial", "udp"], - default="zmq", - ) - parser.add_argument( - "-T", - "--topic", - type=str, - default="#", - help="Topic to subscribe to, valid for zmq and mqtt only.", - ) - parser.add_argument( - "-H", - "--host", - type=str, - help="Host or broker for MQTT, zmq and UDP.", - ) - parser.add_argument( - "-P", - "--port", - type=int, - help="Port or serial interface for MQTT, zmq and UDP.", - ) - parser.add_argument("-v", "--verbose", action="store_true") - parser.add_argument("-p", "--pretty", action="store_true", help="Pretty print JSON data.") - - return parser.parse_args() - - -def keyboardexit(func) -> Callable: - def wrapper(*args, **kwargs) -> Union[None, int]: - try: - return func(*args, **kwargs) - except KeyboardInterrupt: - print("Exiting...") - sys.exit(0) - - return wrapper - - -@keyboardexit -def main() -> None: - args = parse_args() - sink = ConsoleSink(pretty=args.pretty, verbose=args.verbose) - - sub_cls, conf_cls = _registered_sources[args.type] - - try: - config = load_config(conf_cls(), args.type, read_commandline=False) - except FileNotFoundError: - print(f"No config file found for {args.type}, using default values and user input.") - config = conf_cls() - - source = sub_cls(config, args.topic) - if isinstance(source, (MqttSubscriber, UdpSubscriber)): - source.config.host = args.host or source.config.host - source.config.port = args.port or source.config.port - elif isinstance(source, ZmqSubscriber): - source.config.host = args.host or source.config.host - source.config.subscriber_port = args.port or source.config.subscriber_port - source.topic = "" if args.topic == "#" else args.topic - elif isinstance(source, UdpSubscriber): - source.config.port = args.port or source.config.port - - source.start() - sink.start() - - while True: - topic, data = source.receive() - sink.send(data, topic) - - -if __name__ == "__main__": - main() diff --git a/heisskleber/run/zmqbroker.py b/heisskleber/run/zmqbroker.py deleted file mode 100644 index 1ef6535..0000000 --- a/heisskleber/run/zmqbroker.py +++ /dev/null @@ -1,12 +0,0 @@ -from heisskleber.broker import start_zmq_broker -from heisskleber.config import load_config -from heisskleber.zmq.config import ZmqConf as BrokerConf - - -def main(): - broker_config = load_config(BrokerConf(), "zmq") - start_zmq_broker(config=broker_config) - - -if __name__ == "__main__": - main() diff --git a/heisskleber/serial/__init__.py b/heisskleber/serial/__init__.py deleted file mode 100644 index 7921657..0000000 --- a/heisskleber/serial/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .config import SerialConf -from .publisher import SerialPublisher -from .subscriber import SerialSubscriber - -__all__ = ["SerialConf", "SerialPublisher", "SerialSubscriber"] diff --git a/heisskleber/serial/config.py b/heisskleber/serial/config.py deleted file mode 100644 index a845f1e..0000000 --- a/heisskleber/serial/config.py +++ /dev/null @@ -1,11 +0,0 @@ -from dataclasses import dataclass - -from heisskleber.config import BaseConf - - -@dataclass -class SerialConf(BaseConf): - port: str = "/dev/serial0" - baudrate: int = 9600 - bytesize: int = 8 - encoding: str = "ascii" diff --git a/heisskleber/serial/forwarder.py b/heisskleber/serial/forwarder.py deleted file mode 100644 index 2dc15c1..0000000 --- a/heisskleber/serial/forwarder.py +++ /dev/null @@ -1,31 +0,0 @@ -from heisskleber.core.types import Source - -from .publisher import SerialPublisher - - -class SerialForwarder: - def __init__(self, subscriber: Source, publisher: SerialPublisher) -> None: - self.sub = subscriber - self.pub = publisher - - """ - Wait for message and forward - """ - - def forward_message(self) -> None: - # collected = {} - # for sub in self.sub: - # topic, data = sub.receive() - # collected.update(data) - topic, data = self.sub.receive() - - # We send the topic and let the publisher decide what to do with it - self.pub.send(data, topic) - - """ - Enter loop and continuously forward messages - """ - - def sub_pub_loop(self) -> None: - while True: - self.forward_message() diff --git a/heisskleber/serial/publisher.py b/heisskleber/serial/publisher.py deleted file mode 100644 index 52c6f38..0000000 --- a/heisskleber/serial/publisher.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -import sys -from typing import Callable, Optional - -import serial - -from heisskleber.core.packer import get_packer -from heisskleber.core.types import Serializable, Sink - -from .config import SerialConf - - -class SerialPublisher(Sink): - serial_connection: serial.Serial - """ - Publisher for serial devices. - Can be used everywhere that a flucto style publishing connection is required. - - Parameters - ---------- - config : SerialConf - Configuration for the serial connection. - pack_func : FunctionType - Function to translate from a dict to a serialized string. - """ - - def __init__( - self, - config: SerialConf, - pack_func: Optional[Callable] = None, # noqa: UP007 - ): - self.config = config - self.pack = pack_func if pack_func else get_packer("serial") - self.is_connected = False - - def start(self) -> None: - """ - Start the serial connection. - """ - try: - self.serial_connection = serial.Serial( - port=self.config.port, - baudrate=self.config.baudrate, - bytesize=self.config.bytesize, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE, - ) - except serial.SerialException: - print(f"Failed to connect to serial device at port {self.config.port}") - sys.exit(1) - - print(f"Successfully connected to serial device at port {self.config.port}") - self.is_connected = True - - def stop(self) -> None: - """ - Stop the serial connection. - """ - if hasattr(self, "serial_connection") and self.serial_connection.is_open: - self.serial_connection.flush() - self.serial_connection.close() - - def send(self, data: dict[str, Serializable], topic: str) -> None: - """ - Takes python dictionary, serializes it according to the packstyle - and sends it to the broker. - - Parameters - ---------- - message : dict - object to be serialized and sent via the serial connection. Usually a dict. - """ - if not self.is_connected: - self.start() - - payload = self.pack(data) - self.serial_connection.write(payload.encode(self.config.encoding)) - self.serial_connection.flush() - if self.config.verbose: - print(f"{topic}: {payload}") - - def __repr__(self) -> str: - return f"SerialPublisher(port={self.config.port}, baudrate={self.config.baudrate}, bytezize={self.config.bytesize}, encoding={self.config.encoding})" - - def __del__(self) -> None: - self.stop() diff --git a/heisskleber/serial/subscriber.py b/heisskleber/serial/subscriber.py deleted file mode 100644 index db3d962..0000000 --- a/heisskleber/serial/subscriber.py +++ /dev/null @@ -1,110 +0,0 @@ -import sys -from collections.abc import Generator -from typing import Callable - -import serial - -from heisskleber.core.types import Source - -from .config import SerialConf - - -class SerialSubscriber(Source): - serial_connection: serial.Serial - """ - Subscriber for serial devices. Connects to a serial port and reads from it. - - Parameters - ---------- - topics : - Placeholder for topic. Not used. - - config : SerialConf - Configuration class for the serial connection. - - unpack_func : FunctionType - Function to translate from a serialized string to a dict. - """ - - def __init__( - self, - config: SerialConf, - topic: str | None = None, - custom_unpack: Callable | None = None, - ): - self.config = config - self.topic = topic - self.unpack = custom_unpack if custom_unpack else lambda x: x # types: ignore - self.is_connected = False - - def start(self) -> None: - """ - Start the serial connection. - """ - try: - self.serial_connection = serial.Serial( - port=self.config.port, - baudrate=self.config.baudrate, - bytesize=self.config.bytesize, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE, - ) - except serial.SerialException: - print(f"Failed to connect to serial device at port {self.config.port}") - sys.exit(1) - print(f"Successfully connected to serial device at port {self.config.port}") - self.is_connected = True - - def stop(self) -> None: - """ - Stop the serial connection. - """ - if hasattr(self, "serial_connection") and self.serial_connection.is_open: - self.serial_connection.flush() - self.serial_connection.close() - - def receive(self) -> tuple[str, dict]: - """ - Wait for data to arrive on the serial port and return it. - - Returns - ------- - :return: (topic, payload) - topic is a placeholder to adhere to the Subscriber interface - payload is a dictionary containing the data from the serial port - """ - if not self.is_connected: - self.start() - - # message is a string - message = next(self.read_serial_port()) - # payload is a dictionary - payload = self.unpack(message) - # port is a placeholder for topic - return self.config.port, payload - - def read_serial_port(self) -> Generator[str, None, None]: - """ - Generator function reading from the serial port. - - Returns - ------- - :return: Generator[str, None, None] - Generator yielding strings read from the serial port - """ - buffer = "" - while True: - try: - buffer = self.serial_connection.readline().decode(self.config.encoding, "ignore") - yield buffer - except UnicodeError as e: - if self.config.verbose: - print(f"Could not decode: {buffer!r}") - print(e) - continue - - def __repr__(self) -> str: - return f"SerialPublisher(port={self.config.port}, baudrate={self.config.baudrate}, bytezize={self.config.bytesize}, encoding={self.config.encoding})" - - def __del__(self) -> None: - self.stop() diff --git a/heisskleber/stream/__init__.py b/heisskleber/stream/__init__.py deleted file mode 100644 index 94c51ac..0000000 --- a/heisskleber/stream/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .config import ResamplerConf -from .joint import Joint -from .resampler import Resampler - -__all__ = ["Resampler", "ResamplerConf", "Joint"] diff --git a/heisskleber/stream/butter.py b/heisskleber/stream/butter.py deleted file mode 100644 index 7426739..0000000 --- a/heisskleber/stream/butter.py +++ /dev/null @@ -1,77 +0,0 @@ -from collections import deque - -import numpy as np -import scipy.signal # type: ignore [import-untyped] -from numpy.typing import NDArray - -from heisskleber.core.types import AsyncSource, Serializable -from heisskleber.stream.filter import Filter - - -class LiveLFilter: - """ - Filter using standard difference equations. - Kudos to Sam Proell https://www.samproell.io/posts/yarppg/yarppg-live-digital-filter/ - """ - - def __init__(self, b: NDArray[np.float64], a: NDArray[np.float64], init_val: float = 0.0) -> None: - """Initialize live filter based on difference equation. - - Args: - b (array-like): numerator coefficients obtained from scipy. - a (array-like): denominator coefficients obtained from scipy. - """ - self.b = b - self.a = a - self._xs = deque([init_val] * len(b), maxlen=len(b)) - self._ys = deque([init_val] * (len(a) - 1), maxlen=len(a) - 1) - - def __call__(self, x: float) -> float: - """Filter incoming data with standard difference equations.""" - self._xs.appendleft(x) - y = np.dot(self.b, self._xs) - np.dot(self.a[1:], self._ys) - y = y / self.a[0] - self._ys.appendleft(y) - - return y # type: ignore [no-any-return] - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(b={self.b}, a={self.a})" - - -class ButterFilter(Filter): - """ - Butterworth filter based on scipy. - - Args: - source (AsyncSource): Source of data. - cutoff_freq (float): Cutoff frequency. - sampling_rate (float): Sampling rate of the input signal, i.e update frequency - btype (str): Type of filter, "high" or "low" - order (int): order of the filter to be applied - - Example: - >>> source = get_async_source() - >>> filter = LowPassFilter(source, 0.1, 100, 3) - >>> async for topic, data in filter: - >>> print(topic, data) - """ - - def __init__( - self, source: AsyncSource, cutoff_freq: float, sampling_rate: float, btype: str = "low", order: int = 3 - ) -> None: - self.source = source - nyquist_fq = sampling_rate / 2 - Wn = cutoff_freq / nyquist_fq - self.b, self.a = scipy.signal.iirfilter(order, Wn=Wn, fs=sampling_rate, btype=btype, ftype="butter") - self.filters: dict[str, LiveLFilter] = {} - - def _filter(self, data: dict[str, Serializable]) -> dict[str, Serializable]: - if not self.filters: - for key in data: - self.filters[key] = LiveLFilter(a=self.a, b=self.b) - - for key, value in data.items(): - data[key] = self.filters[key](value) - - return data diff --git a/heisskleber/stream/config.py b/heisskleber/stream/config.py deleted file mode 100644 index 1e3ad90..0000000 --- a/heisskleber/stream/config.py +++ /dev/null @@ -1,6 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class ResamplerConf: - resample_rate: int = 1000 diff --git a/heisskleber/stream/filter.py b/heisskleber/stream/filter.py deleted file mode 100644 index 22bc558..0000000 --- a/heisskleber/stream/filter.py +++ /dev/null @@ -1,19 +0,0 @@ -from abc import ABC, abstractmethod -from collections.abc import AsyncGenerator -from typing import Any - -from heisskleber.core.types import AsyncSource, Serializable - - -class Filter(ABC): - def __init__(self, source: AsyncSource): - self.source = source - - async def __aiter__(self) -> AsyncGenerator[Any, None]: - async for topic, data in self.source: - data = self._filter(data) - yield topic, data - - @abstractmethod - def _filter(self, data: dict[str, Serializable]) -> dict[str, Serializable]: - pass diff --git a/heisskleber/stream/gh-filter.py b/heisskleber/stream/gh-filter.py deleted file mode 100644 index 6c857b5..0000000 --- a/heisskleber/stream/gh-filter.py +++ /dev/null @@ -1,56 +0,0 @@ -from heisskleber.core.types import AsyncSource -from heisskleber.stream.filter import Filter - - -class GhFilter(Filter): - """ - G-H filter (also called alpha-beta, f-g filter), simplified observer for estimation and data smoothing. - - Args: - source (AsyncSource): Source of data. - g (float): Correction gain for value - h (float): Correction gain for derivative - - Example: - >>> source = get_async_source() - >>> filter = GhFilter(source, 0.008, 0.001) - >>> async for topic, data in filter: - >>> print(topic, data) - """ - - def __init__(self, source: AsyncSource, g: float, h: float): - self.source = source - if not 0 < g < 1.0 or not 0 < h < 1.0: - msg = "g and h must be between 0 and 1.0" - raise ValueError(msg) - self.g = g - self.h = h - self.x: dict[str, float] = {} - self.dx: dict[str, float] = {} - - def _filter(self, data: dict[str, float]) -> dict[str, float]: - if not self.x: - self.x = data - self.dx = {key: 0.0 for key in data} - return data - - invalid_keys = [] - ts = data.pop("epoch") - dt = ts - self.x["epoch"] - if abs(dt) <= 1e-4: - data["epoch"] = ts - return data - - for key in data: - if not isinstance(data[key], float): - invalid_keys.append(key) - continue - x_pred = self.x[key] + dt * self.dx[key] - residual = data[key] - x_pred - self.dx[key] = self.dx[key] + self.h * residual / dt - self.x[key] = x_pred + self.g * residual - - for key in invalid_keys: - self.x[key] = data[key] - self.x["epoch"] = ts - return self.x diff --git a/heisskleber/stream/joint.py b/heisskleber/stream/joint.py deleted file mode 100644 index ed8102b..0000000 --- a/heisskleber/stream/joint.py +++ /dev/null @@ -1,114 +0,0 @@ -import asyncio -from typing import Any - -from heisskleber.core.types import Serializable -from heisskleber.stream.resampler import Resampler, ResamplerConf - - -class Joint: - """Joint that takes multiple async streams and synchronizes them based on their timestamps. - - Note that you need to run the setup() function first to initialize the - - Parameters: - ---------- - conf : ResamplerConf - Configuration for the joint. - subscribers : list[AsyncSubscriber] - List of asynchronous subscribers. - - """ - - def __init__(self, conf: ResamplerConf, resamplers: list[Resampler]): - self.conf = conf - self.resamplers = resamplers - self.output_queue: asyncio.Queue[dict[str, Serializable]] = asyncio.Queue() - self.initialized = asyncio.Event() - self.initalize_task = asyncio.create_task(self.sync()) - self.combined_dict: dict[str, Serializable] = {} - self.task: asyncio.Task[None] | None = None - - def __repr__(self) -> str: - return f"""Joint(resample_rate={self.conf.resample_rate}, - sources={len(self.resamplers)} of type(s): {{r.__class__.__name__ for r in self.resamplers}})""" - - def start(self) -> None: - self.task = asyncio.create_task(self.output_work()) - - def stop(self) -> None: - if self.task: - self.task.cancel() - - async def receive(self) -> dict[str, Any]: - """ - Main interaction coroutine: Get next value out of the queue. - """ - if not self.task: - self.start() - output = await self.output_queue.get() - return output - - async def sync(self) -> None: - """Synchronize the resamplers by pulling data from each until the timestamp is aligned. Retains first matching data.""" - print("Starting sync") - datas = await asyncio.gather(*[source.receive() for source in self.resamplers]) - print("Got data") - output_data = {} - data = {} - - latest_timestamp: float = 0.0 - timestamps = [] - - print("Syncing...") - for data in datas: - if not isinstance(data["epoch"], float): - error = "Timestamps must be floats" - raise TypeError(error) - - ts = float(data["epoch"]) - - print(f"Syncing..., got {ts}") - - timestamps.append(ts) - if ts > latest_timestamp: - latest_timestamp = ts - - # only take the piece of the latest data - output_data = data - - for resampler, ts in zip(self.resamplers, timestamps): - while ts < latest_timestamp: - data = await resampler.receive() - ts = float(data["epoch"]) - - output_data.update(data) - - await self.output_queue.put(output_data) - - print("Finished initalization") - self.initialized.set() - - """ - Coroutine that waits for new queue data and updates dict. - """ - - async def update_dict(self, resampler: Resampler) -> None: - data = await resampler.receive() - if self.combined_dict and self.combined_dict["epoch"] != data["epoch"]: - print("Oh shit, this is bad!") - self.combined_dict.update(data) - - """ - Output worker: iterate through queues, read data and join into output queue. - """ - - async def output_work(self) -> None: - print("Output worker waiting for intitialization") - await self.initialized.wait() - print("Output worker resuming") - - while True: - self.combined_dict = {} - tasks = [asyncio.create_task(self.update_dict(res)) for res in self.resamplers] - await asyncio.gather(*tasks) - await self.output_queue.put(self.combined_dict) diff --git a/heisskleber/stream/resampler.py b/heisskleber/stream/resampler.py deleted file mode 100644 index f6d0ea7..0000000 --- a/heisskleber/stream/resampler.py +++ /dev/null @@ -1,195 +0,0 @@ -import math -from asyncio import Queue, Task, create_task -from collections.abc import Generator -from datetime import datetime, timedelta - -import numpy as np - -from heisskleber.core.types import AsyncSource, Serializable - -from .config import ResamplerConf - - -def floor_dt(dt: datetime, delta: timedelta) -> datetime: - """Round a datetime object based on a delta timedelta.""" - return datetime.min + math.floor((dt - datetime.min) / delta) * delta - - -def timestamp_generator(start_epoch: float, timedelta_in_ms: int) -> Generator[float, None, None]: - """Generate increasing timestamps based on a start epoch and a delta in ms. - The timestamps are meant to be used with the resampler and generator half delta offsets of the returned timetsamps. - """ - timestamp_start = datetime.fromtimestamp(start_epoch) - delta = timedelta(milliseconds=timedelta_in_ms) - delta_half = timedelta(milliseconds=timedelta_in_ms // 2) - next_timestamp = floor_dt(timestamp_start, delta) + delta_half - while True: - yield datetime.timestamp(next_timestamp) - next_timestamp += delta - - -def interpolate(t1: float, y1: list[float], t2: float, y2: list[float], t_target: float) -> list[float]: - """Perform linear interpolation between two data points.""" - y1_array, y2_array = np.array(y1), np.array(y2) - fraction = (t_target - t1) / (t2 - t1) - interpolated_values = y1_array + fraction * (y2_array - y1_array) - return interpolated_values.tolist() - - -def check_dict(data: dict[str, Serializable]) -> None: - """Check that only numeric types are in input data.""" - for key, value in data.items(): - if not isinstance(value, (int, float)): - error_msg = f"Value {value} for key {key} is not of type int or float" - raise TypeError(error_msg) - - -class Resampler: - """ - Async resample data based on a fixed rate. Can handle upsampling and downsampling. - - Methods: - -------- - start() - Start the resampler task. - - stop() - Stop the resampler task. - - receive() - Get next resampled dictonary from the resampler. - """ - - def __init__(self, config: ResamplerConf, subscriber: AsyncSource) -> None: - """ - Parameters: - ---------- - config : namedtuple - Configuration for the resampler. - subscriber : AsyncMQTTSubscriber - Asynchronous Subscriber - - """ - self.config = config - self.subscriber = subscriber - self.resample_rate = self.config.resample_rate - self.delta_t = round(self.resample_rate / 1_000, 3) - self.message_queue: Queue[dict[str, float]] = Queue(maxsize=50) - self.resample_task: None | Task[None] = None - - def start(self) -> None: - """ - Start the resampler task. - """ - self.resample_task = create_task(self.resample()) - - def stop(self) -> None: - """ - Stop the resampler task - """ - if self.resample_task: - self.resample_task.cancel() - - async def receive(self) -> dict[str, float]: - """ - Get next resampled dictonary from the resampler. - - Implicitly starts the resampler if not already running. - """ - if not self.resample_task: - self.start() - return await self.message_queue.get() - - async def resample(self) -> None: - """ - Resample data based on a fixed rate. - - Can handle upsampling and downsampling. - Data will always be centered around the output resample timestamp. - (i.e. for data returned for t = 1.0s, the data will be resampled for [0.5, 1.5]s) - """ - - print("Starting resampler") - aggregated_data = [] - aggregated_timestamps = [] - - # Get first element to determine timestamp - topic, data = await self.subscriber.receive() - - check_dict(data) - - timestamp, message = self._pack_data(data) # type: ignore [arg-type] - timestamps = timestamp_generator(timestamp, self.resample_rate) - print(f"Got first element {topic}: {data}") - - # Set data keys to reconstruct dict later - self.data_keys = data.keys() - self.topic = topic - - # step through interpolation timestamps - for next_timestamp in timestamps: - # await new data and append to buffer until the most recent data - # is newer than the next interplation timestamp - while timestamp < next_timestamp: - aggregated_timestamps.append(timestamp) - aggregated_data.append(message) - - topic, data = await self.subscriber.receive() - timestamp, message = self._pack_data(data) # type: ignore [arg-type] - - return_timestamp = round(next_timestamp - self.delta_t / 2, 3) - - # Only one new data point was received - if len(aggregated_data) == 1: - self._is_upsampling = False - # print("Only one data point") - last_timestamp, last_message = ( - aggregated_timestamps[0], - aggregated_data[0], - ) - - # Case 2 Upsampling: - while timestamp - next_timestamp > self.delta_t: - self._is_upsampling = True - # print("Upsampling") - last_message = interpolate( - last_timestamp, - last_message, - timestamp, - message, - return_timestamp, - ) - last_timestamp = return_timestamp - return_timestamp += self.delta_t - next_timestamp = next(timestamps) - await self.message_queue.put(self._unpack_data(last_timestamp, last_message)) - - if self._is_upsampling: - last_message = interpolate( - last_timestamp, - last_message, - timestamp, - message, - return_timestamp, - ) - last_timestamp = return_timestamp - - await self.message_queue.put(self._unpack_data(last_timestamp, last_message)) - - if len(aggregated_data) > 1: - # Case 4 - downsampling: Multiple data points were during the resampling timeframe - mean_message = np.mean(np.array(aggregated_data), axis=0) - await self.message_queue.put(self._unpack_data(return_timestamp, mean_message)) - - # reset the aggregator - aggregated_data.clear() - aggregated_timestamps.clear() - - def _pack_data(self, data: dict[str, float]) -> tuple[float, list[float]]: - # pack data from dict to tuple list - ts = data.pop("epoch") - return (ts, list(data.values())) - - def _unpack_data(self, ts: float, values: list[float]) -> dict[str, float]: - # from tuple - return {"epoch": round(ts, 3), **dict(zip(self.data_keys, values))} diff --git a/heisskleber/stream/sync_resampler.py b/heisskleber/stream/sync_resampler.py deleted file mode 100644 index 6dcc5c0..0000000 --- a/heisskleber/stream/sync_resampler.py +++ /dev/null @@ -1,142 +0,0 @@ -import math -from datetime import datetime, timedelta -from queue import Queue - -import numpy as np - -from heisskleber.mqtt import MqttSubscriber - - -def round_dt(dt, delta): - """Round a datetime object based on a delta timedelta.""" - return datetime.min + math.floor((dt - datetime.min) / delta) * delta - - -def timestamp_generator(start_epoch, timedelta_in_ms): - """Generate increasing timestamps based on a start epoch and a delta in ms.""" - timestamp_start = datetime.fromtimestamp(start_epoch) - delta = timedelta(milliseconds=timedelta_in_ms) - delta_half = timedelta(milliseconds=timedelta_in_ms // 2) - next_timestamp = round_dt(timestamp_start, delta) + delta_half - while True: - yield datetime.timestamp(next_timestamp) - next_timestamp += delta - - -def interpolate(t1, y1, t2, y2, t_target): - """Perform linear interpolation between two data points.""" - y1, y2 = np.array(y1), np.array(y2) - fraction = (t_target - t1) / (t2 - t1) - interpolated_values = y1 + fraction * (y2 - y1) - return interpolated_values.tolist() - - -class Resampler: - """ - Synchronously resample data based on a fixed rate. Can handle upsampling and downsampling. - - Parameters: - ---------- - config : namedtuple - Configuration for the resampler. - subscriber : MqttSubscriber - Synchronous Subscriber - """ - - def __init__(self, config, subscriber: MqttSubscriber): - self.config = config - self.subscriber = subscriber - self.buffer = Queue() - self.resample_rate = self.config.resample_rate - self.delta_t = round(self.resample_rate / 1_000, 3) - - def run(self): - topic, message = self.subscriber.receive() - self.buffer.put(self._pack_data(message)) - self.data_keys = message.keys() - - while True: - topic, message = self.subscriber.receive() - self.buffer.put(self._pack_data(message)) - - def resample(self): - aggregated_data = [] - aggregated_timestamps = [] - # Get first element to determine timestamp - timestamp, message = self.buffer.get() - timestamps = timestamp_generator(timestamp, self.resample_rate) - - # step through interpolation timestamps - for next_timestamp in timestamps: - # last_timestamp, last_message = timestamp, message - - # append new data to buffer until the most recent data - # is newer than the next interplation timestamp - while timestamp < next_timestamp: - aggregated_timestamps.append(timestamp) - aggregated_data.append(message) - timestamp, message = self.buffer.get() - - return_timestamp = round(next_timestamp - self.delta_t / 2, 3) - - # Case 1: Only one new data point was received - if len(aggregated_data) == 1: - last_timestamp, last_message = ( - aggregated_timestamps[0], - aggregated_data[0], - ) - - # Case 1a Upsampling: - # The data point is not within our time interval - # We step through time intervals, yielding interpolated data points - while timestamp - next_timestamp > self.delta_t: - last_message = interpolate( - last_timestamp, - last_message, - timestamp, - message, - return_timestamp, - ) - last_timestamp = return_timestamp - return_timestamp += self.delta_t - next_timestamp = next(timestamps) - yield self._unpack_data(last_timestamp, last_message) - - # Case 1b: The data point is within our time interval - # We simply yield the data point - # Note, this will also be the case once we have advanced the time interval by upsampling - last_message = interpolate( - last_timestamp, - last_message, - timestamp, - message, - return_timestamp, - ) - last_timestamp = return_timestamp - return_timestamp += self.delta_t - yield self._unpack_data(last_timestamp, last_message) - - # Case 2 - downsampling: Multiple data points were during the resampling timeframe - # We simply yield the mean of the data points, which is more robust and performant than interpolation - if len(aggregated_data) > 1: - # yield self._handle_downsampling(return_timestamp, aggregated_data) - mean_message = np.mean(np.array(aggregated_data), axis=0) - yield self._unpack_data(return_timestamp, mean_message) - - # reset the aggregator - aggregated_data.clear() - aggregated_timestamps.clear() - - def _handle_downsampling(self, return_timestamp, aggregated_data) -> dict: - """Handle the downsampling case.""" - mean_message = np.mean(np.array(aggregated_data), axis=0) - return self._unpack_data(return_timestamp, mean_message) - - def _pack_data(self, data) -> tuple[int, list]: - # pack data from dict to tuple list - ts = data.pop("epoch") - return (ts, list(data.values())) - - def _unpack_data(self, ts, values) -> dict: - # from tuple - return {"epoch": round(ts, 3), **dict(zip(self.data_keys, values))} diff --git a/heisskleber/tcp/__init__.py b/heisskleber/tcp/__init__.py deleted file mode 100644 index 4e165dd..0000000 --- a/heisskleber/tcp/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from heisskleber.tcp.config import TcpConf -from heisskleber.tcp.sink import AsyncTcpSink -from heisskleber.tcp.source import AsyncTcpSource - -__all__ = ["AsyncTcpSource", "AsyncTcpSink", "TcpConf"] diff --git a/heisskleber/tcp/config.py b/heisskleber/tcp/config.py deleted file mode 100644 index 6ad76ce..0000000 --- a/heisskleber/tcp/config.py +++ /dev/null @@ -1,10 +0,0 @@ -from dataclasses import dataclass - -from heisskleber.config import BaseConf - - -@dataclass -class TcpConf(BaseConf): - host: str = "localhost" - port: int = 6000 - timeout: int = 60 diff --git a/heisskleber/tcp/source.py b/heisskleber/tcp/source.py deleted file mode 100644 index d34854f..0000000 --- a/heisskleber/tcp/source.py +++ /dev/null @@ -1,60 +0,0 @@ -import asyncio -from typing import Callable - -from heisskleber.core.types import AsyncSource, Serializable -from heisskleber.tcp.config import TcpConf - - -def bytes_csv_unpacker(data: bytes) -> tuple[str, dict[str, str]]: - vals = data.decode().rstrip().split(",") - keys = [f"key{i}" for i in range(len(vals))] - return ("tcp", dict(zip(keys, vals))) - - -class AsyncTcpSource(AsyncSource): - """ - Async TCP connection, connects to host:port and reads byte encoded strings. - - - Pass an unpack function like so: - - Example - ------- - def unpack(data: bytes) -> tuple[str, dict[str, float | int | str]]: - return dict(zip(["key1", "key2"], data.decode().split(",")) - - """ - - def __init__(self, config: TcpConf, unpack: Callable[[bytes], tuple[str, dict[str, Serializable]]] | None) -> None: - self.config = config - self.is_connected = asyncio.Event() - self.unpack = unpack or bytes_csv_unpacker - self.timeout = config.timeout - self.start_task: asyncio.Task[None] | None = None - - async def receive(self) -> tuple[str, dict[str, Serializable]]: - await self._check_connection() - data = await self.reader.readline() - topic, payload = self.unpack(data) - return (topic, payload) # type: ignore - - def start(self) -> None: - self.start_task = asyncio.create_task(self._connect()) - - def stop(self) -> None: - if self.is_connected: - print("stopping") - - async def _check_connection(self) -> None: - if not self.start_task: - self.start() - await self.is_connected.wait() - - async def _connect(self) -> None: - print(f"{self} waiting for connection.") - (self.reader, self.writer) = await asyncio.open_connection(self.config.host, self.config.port) - print(f"{self} connected successfully!") - self.is_connected.set() - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.host}, port={self.config.port})" diff --git a/heisskleber/udp/__init__.py b/heisskleber/udp/__init__.py deleted file mode 100644 index c550407..0000000 --- a/heisskleber/udp/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .config import UdpConf -from .publisher import AsyncUdpSink, UdpPublisher -from .subscriber import AsyncUdpSource, UdpSubscriber - -__all__ = ["AsyncUdpSource", "UdpSubscriber", "AsyncUdpSink", "UdpPublisher", "UdpConf"] diff --git a/heisskleber/udp/publisher.py b/heisskleber/udp/publisher.py deleted file mode 100644 index ffe88ae..0000000 --- a/heisskleber/udp/publisher.py +++ /dev/null @@ -1,84 +0,0 @@ -import asyncio -import socket -import sys - -from heisskleber.core.packer import get_packer -from heisskleber.core.types import AsyncSink, Serializable, Sink -from heisskleber.udp.config import UdpConf - - -class UdpPublisher(Sink): - def __init__(self, config: UdpConf) -> None: - self.config = config - self.pack = get_packer(self.config.packer) - self.is_connected = False - - def start(self) -> None: - try: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - except OSError as e: - print(f"failed to create socket: {e}") - sys.exit(-1) - else: - self.is_connected = True - - def stop(self) -> None: - self.socket.close() - self.is_connected = True - - def send(self, data: dict[str, Serializable], topic: str | None = None) -> None: - if not self.is_connected: - self.start() - if topic: - data["topic"] = topic - payload = self.pack(data).encode("utf-8") - self.socket.sendto(payload, (self.config.host, self.config.port)) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.host}, port={self.config.port})" - - -class UdpProtocol(asyncio.DatagramProtocol): - def __init__(self, is_connected: bool) -> None: - super().__init__() - self.is_connected = is_connected - - def connection_lost(self, exc: Exception | None) -> None: - print("Connection lost") - self.is_connected = False - - -class AsyncUdpSink(AsyncSink): - def __init__(self, config: UdpConf) -> None: - self.config = config - self.pack = get_packer(self.config.packer) - self.socket: asyncio.DatagramTransport | None = None - self.is_connected = False - - def start(self) -> None: - # No background loop required - pass - - def stop(self) -> None: - if self.socket is not None: - self.socket.close() - self.is_connected = False - - async def _ensure_connection(self) -> None: - if not self.is_connected: - loop = asyncio.get_running_loop() - self.socket, _ = await loop.create_datagram_endpoint( - lambda: UdpProtocol(self.is_connected), - remote_addr=(self.config.host, self.config.port), - ) - self.is_connected = True - - async def send(self, data: dict[str, Serializable], topic: str | None = None) -> None: - await self._ensure_connection() - if topic: - data["topic"] = topic - payload = self.pack(data).encode(self.config.encoding) - self.socket.sendto(payload) # type: ignore - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.host}, port={self.config.port})" diff --git a/heisskleber/udp/subscriber.py b/heisskleber/udp/subscriber.py deleted file mode 100644 index 5925224..0000000 --- a/heisskleber/udp/subscriber.py +++ /dev/null @@ -1,117 +0,0 @@ -import asyncio -import socket -import sys -import threading -from queue import Queue -from typing import Any - -from heisskleber.core.packer import get_unpacker -from heisskleber.core.types import AsyncSource, Serializable, Source -from heisskleber.udp.config import UdpConf - - -class UdpSubscriber(Source): - def __init__(self, config: UdpConf, topic: str | None = None): - self.config = config - self.topic = topic - self.unpacker = get_unpacker(self.config.packer) - self._queue: Queue[tuple[str, dict[str, Serializable]]] = Queue(maxsize=self.config.max_queue_size) - self._running = threading.Event() - - def start(self) -> None: - try: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - except OSError as e: - print(f"failed to create socket: {e}") - sys.exit(-1) - self.socket.bind((self.config.host, self.config.port)) - self._running.set() - self._thread = threading.Thread(target=self._loop, daemon=True) - self._thread.start() - - def stop(self) -> None: - self._running.clear() - # if self._thread is not None: - # self._thread.join() - self.socket.close() - - def receive(self) -> tuple[str, dict[str, Serializable]]: - if not self._running.is_set(): - self.start() - return self._queue.get() - - def _loop(self) -> None: - while self._running.is_set(): - try: - payload, _ = self.socket.recvfrom(1024) - data = self.unpacker(payload.decode("utf-8")) - topic: str = str(data.pop("topic")) if "topic" in data else "" - self._queue.put((topic, data)) - except Exception as e: - error_message = f"Error in UDP listener loop: {e}" - print(error_message) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.host}, port={self.config.port})" - - -class UdpProtocol(asyncio.DatagramProtocol): - def __init__(self, queue: asyncio.Queue[bytes]) -> None: - super().__init__() - self.queue = queue - - def datagram_received(self, data: bytes, addr: tuple[str | Any, int]) -> None: - self.queue.put_nowait(data) - - def connection_made(self, transport: asyncio.DatagramTransport) -> None: - print("Connection made") - - -class AsyncUdpSource(AsyncSource): - """ - An asynchronous UDP subscriber based on asyncio.protocols.DatagramProtocol - """ - - def __init__(self, config: UdpConf, topic: str = "udp"): - self.config = config - self.topic = topic - self.EOF = self.config.delimiter.encode(self.config.encoding) - self.unpacker = get_unpacker(self.config.packer) - self.queue: asyncio.Queue[bytes] = asyncio.Queue(maxsize=self.config.max_queue_size) - self.task: asyncio.Task[None] | None = None - self.is_connected = False - - async def setup(self) -> None: - loop = asyncio.get_event_loop() - self.transport, self.protocol = await loop.create_datagram_endpoint( - lambda: UdpProtocol(self.queue), - local_addr=(self.config.host, self.config.port), - ) - self.is_connected = True - print("Udp connection established.") - - def start(self) -> None: - # Background loop not required, handled by Protocol - pass - - def stop(self) -> None: - self.transport.close() - - async def receive(self) -> tuple[str, dict[str, Serializable]]: - if not self.is_connected: - await self.setup() - data = await self.queue.get() - try: - payload = self.unpacker(data.decode(self.config.encoding, errors="ignore")) - # except UnicodeDecodeError: # this won't be thrown anymore, as the error flag is set to ignore! - # print(f"Could not decode data, is not {self.config.encoding}") - except Exception: - if self.config.verbose: - print(f"Could not deserialize data: {data!r}") - else: - return (self.topic, payload) - - return await self.receive() # Try again - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.host}, port={self.config.port})" diff --git a/heisskleber/zmq/__init__.py b/heisskleber/zmq/__init__.py deleted file mode 100644 index 5f2019c..0000000 --- a/heisskleber/zmq/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .config import ZmqConf -from .publisher import ZmqAsyncPublisher, ZmqPublisher -from .subscriber import ZmqAsyncSubscriber, ZmqSubscriber - -__all__ = ["ZmqConf", "ZmqPublisher", "ZmqSubscriber", "ZmqAsyncPublisher", "ZmqAsyncSubscriber"] diff --git a/heisskleber/zmq/publisher.py b/heisskleber/zmq/publisher.py deleted file mode 100644 index 22abffc..0000000 --- a/heisskleber/zmq/publisher.py +++ /dev/null @@ -1,129 +0,0 @@ -import sys -from typing import Callable - -import zmq -import zmq.asyncio - -from heisskleber.core.packer import get_packer -from heisskleber.core.types import AsyncSink, Serializable, Sink - -from .config import ZmqConf - - -class ZmqPublisher(Sink): - """ - Publisher that sends messages to a ZMQ PUB socket. - - Attributes: - ----------- - pack : Callable - The packer function to use for serializing the data. - - Methods: - -------- - send(data : dict, topic : str): - Send the data with the given topic. - - start(): - Connect to the socket. - - stop(): - Close the socket. - """ - - def __init__(self, config: ZmqConf): - self.config = config - self.context = zmq.Context.instance() - self.socket = self.context.socket(zmq.PUB) - self.pack = get_packer(config.packstyle) - self.is_connected = False - - def send(self, data: dict[str, Serializable], topic: str) -> None: - """ - Take the data as a dict, serialize it with the given packer and send it to the zmq socket. - """ - if not self.is_connected: - self.start() - payload = self.pack(data) - if self.config.verbose: - print(f"sending message {payload} to topic {topic}") - self.socket.send_multipart([topic.encode(), payload.encode()]) - - def start(self) -> None: - """Connect to the zmq socket.""" - try: - if self.config.verbose: - print(f"connecting to {self.config.publisher_address}") - self.socket.connect(self.config.publisher_address) - except Exception as e: - print(f"failed to bind to zeromq socket: {e}") - sys.exit(-1) - else: - self.is_connected = True - - def stop(self) -> None: - self.socket.close() - self.is_connected = False - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.publisher_address}, port={self.config.publisher_port})" - - -class ZmqAsyncPublisher(AsyncSink): - """ - Async publisher that sends messages to a ZMQ PUB socket. - - Attributes: - ----------- - pack : Callable - The packer function to use for serializing the data. - - Methods: - -------- - send(data : dict, topic : str): - Send the data with the given topic. - - start(): - Connect to the socket. - - stop(): - Close the socket. - """ - - def __init__(self, config: ZmqConf): - self.config = config - self.context = zmq.asyncio.Context.instance() - self.socket: zmq.asyncio.Socket = self.context.socket(zmq.PUB) - self.pack: Callable = get_packer(config.packstyle) - self.is_connected = False - - async def send(self, data: dict[str, Serializable], topic: str) -> None: - """ - Take the data as a dict, serialize it with the given packer and send it to the zmq socket. - """ - if not self.is_connected: - self.start() - payload = self.pack(data) - if self.config.verbose: - print(f"sending message {payload} to topic {topic}") - await self.socket.send_multipart([topic.encode(), payload.encode()]) - - def start(self) -> None: - """Connect to the zmq socket.""" - try: - if self.config.verbose: - print(f"connecting to {self.config.publisher_address}") - self.socket.connect(self.config.publisher_address) - except Exception as e: - print(f"failed to bind to zeromq socket: {e}") - sys.exit(-1) - else: - self.is_connected = True - - def stop(self) -> None: - """Close the zmq socket.""" - self.socket.close() - self.is_connected = False - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.publisher_address}, port={self.config.publisher_port})" diff --git a/heisskleber/zmq/subscriber.py b/heisskleber/zmq/subscriber.py deleted file mode 100644 index ff33bf9..0000000 --- a/heisskleber/zmq/subscriber.py +++ /dev/null @@ -1,181 +0,0 @@ -from __future__ import annotations - -import sys - -import zmq -import zmq.asyncio - -from heisskleber.core.packer import get_unpacker -from heisskleber.core.types import AsyncSource, Source - -from .config import ZmqConf - - -class ZmqSubscriber(Source): - """ - Source that subscribes to one or many topics from a zmq broker and receives messages via the receive() function. - - Attributes: - ----------- - unpack : Callable - The unpacker function to use for deserializing the data. - - Methods: - -------- - receive() -> tuple[str, dict]: - Send the data with the given topic. - - start(): - Connect to the socket. - - stop(): - Close the socket. - """ - - def __init__(self, config: ZmqConf, topic: str | list[str]): - """ - Constructs new ZmqAsyncSubscriber instance. - - Parameters: - ----------- - config : ZmqConf - The configuration dataclass object for the zmq connection. - topic : str - The topic or list of topics to subscribe to. - """ - self.config = config - self.topic = topic - self.context = zmq.Context.instance() - self.socket = self.context.socket(zmq.SUB) - self.unpack = get_unpacker(config.packstyle) - self.is_connected = False - - def receive(self) -> tuple[str, dict]: - """ - reads a message from the zmq bus and returns it - - Returns: - tuple(topic: str, message: dict): the message received - """ - if not self.is_connected: - self.start() - (topic, payload) = self.socket.recv_multipart() - message = self.unpack(payload.decode()) - topic = topic.decode() - return (topic, message) - - def start(self): - try: - self.socket.connect(self.config.subscriber_address) - self.subscribe(self.topic) - except Exception as e: - print(f"failed to bind to zeromq socket: {e}") - sys.exit(-1) - else: - self.is_connected = True - - def stop(self): - self.socket.close() - self.is_connected = False - - def subscribe(self, topic: str | list[str] | tuple[str]): - # Accepts single topic or list of topics - if isinstance(topic, (list, tuple)): - for t in topic: - self._subscribe_single_topic(t) - else: - self._subscribe_single_topic(topic) - - def _subscribe_single_topic(self, topic: str): - self.socket.setsockopt(zmq.SUBSCRIBE, topic.encode()) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.subscriber_address}, port={self.config.subscriber_port})" - - -class ZmqAsyncSubscriber(AsyncSource): - """ - Async source that subscribes to one or many topics from a zmq broker and receives messages via the receive() function. - - Attributes: - ----------- - unpack : Callable - The unpacker function to use for deserializing the data. - - Methods: - -------- - receive() -> tuple[str, dict]: - Send the data with the given topic. - - start(): - Connect to the socket. - - stop(): - Close the socket. - """ - - def __init__(self, config: ZmqConf, topic: str | list[str]): - """ - Constructs new ZmqAsyncSubscriber instance. - - Parameters: - ----------- - config : ZmqConf - The configuration dataclass object for the zmq connection. - topic : str - The topic or list of topics to subscribe to. - """ - self.config = config - self.topic = topic - self.context = zmq.asyncio.Context.instance() - self.socket: zmq.asyncio.Socket = self.context.socket(zmq.SUB) - self.unpack = get_unpacker(config.packstyle) - self.is_connected = False - - async def receive(self) -> tuple[str, dict]: - """ - reads a message from the zmq bus and returns it - - Returns: - tuple(topic: str, message: dict): the message received - """ - if not self.is_connected: - self.start() - (topic, payload) = await self.socket.recv_multipart() - message = self.unpack(payload.decode()) - topic = topic.decode() - return (topic, message) - - def start(self): - """Connect to the zmq socket.""" - try: - self.socket.connect(self.config.subscriber_address) - except Exception as e: - print(f"failed to bind to zeromq socket: {e}") - sys.exit(-1) - else: - self.is_connected = True - self.subscribe(self.topic) - - def stop(self): - """Close the zmq socket.""" - self.socket.close() - self.is_connected = False - - def subscribe(self, topic: str | list[str] | tuple[str]): - """ - Subscribes to the given topic(s) on the zmq socket. - - Accepts single topic or list of topics. - """ - if isinstance(topic, (list, tuple)): - for t in topic: - self._subscribe_single_topic(t) - else: - self._subscribe_single_topic(topic) - - def _subscribe_single_topic(self, topic: str): - self.socket.setsockopt(zmq.SUBSCRIBE, topic.encode()) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(host={self.config.subscriber_address}, port={self.config.subscriber_port})" diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..64fcf0b --- /dev/null +++ b/noxfile.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +import nox + +DIR = Path(__file__).parent.resolve() + +nox.needs_version = ">=2024.3.2" +nox.options.sessions = ["lint", "tests", "check"] +nox.options.default_venv_backend = "uv|virtualenv" + + +@nox.session +def lint(session: nox.Session) -> None: + """Run the linter.""" + session.install("pre-commit") + session.run("pre-commit", "run", "--all-files", "--show-diff-on-failure", *session.posargs) + + +@nox.session +def tests(session: nox.Session) -> None: + """Run the unit and regular tests.""" + session.install(".[test]") + session.run("pytest", *session.posargs) + + +@nox.session(reuse_venv=True) +def docs(session: nox.Session) -> None: + """Build the docs. Pass --non-interactive to avoid serving. First positional argument is the target directory.""" + parser = argparse.ArgumentParser() + parser.add_argument("-b", dest="builder", default="html", help="Build target (default: html)") + parser.add_argument("output", nargs="?", help="Output directory") + args, posargs = parser.parse_known_args(session.posargs) + serve = args.builder == "html" and session.interactive + + session.install("-e.[docs]", "sphinx-autobuild") + + shared_args = ( + "-n", # nitpicky mode + "-T", # full tracebacks + f"-b={args.builder}", + "docs", + args.output or f"docs/_build/{args.builder}", + *posargs, + ) + + if serve: + session.run("sphinx-autobuild", "--open-browser", *shared_args) + else: + session.run("sphinx-build", "--keep-going", *shared_args) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 9a8ef17..0000000 --- a/poetry.lock +++ /dev/null @@ -1,2083 +0,0 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. - -[[package]] -name = "aiomqtt" -version = "1.2.1" -description = "The idiomatic asyncio MQTT client, wrapped around paho-mqtt" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "aiomqtt-1.2.1-py3-none-any.whl", hash = "sha256:3925b40b2b95b1905753d53ef3a9162e903cfab35ebe9647ab4d52e45ffb727f"}, - {file = "aiomqtt-1.2.1.tar.gz", hash = "sha256:7582f4341f08ef7110dd9ab3a559454dc28ccda1eac502ff8f08a73b238ecede"}, -] - -[package.dependencies] -paho-mqtt = ">=1.6.0,<2.0.0" -typing-extensions = {version = ">=4.4.0,<5.0.0", markers = "python_version < \"3.10\""} - -[[package]] -name = "alabaster" -version = "0.7.16" -description = "A light, configurable Sphinx theme" -optional = false -python-versions = ">=3.9" -files = [ - {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, - {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, -] - -[[package]] -name = "authlib" -version = "1.3.0" -description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." -optional = false -python-versions = ">=3.8" -files = [ - {file = "Authlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:9637e4de1fb498310a56900b3e2043a206b03cb11c05422014b0302cbc814be3"}, - {file = "Authlib-1.3.0.tar.gz", hash = "sha256:959ea62a5b7b5123c5059758296122b57cd2585ae2ed1c0622c21b371ffdae06"}, -] - -[package.dependencies] -cryptography = "*" - -[[package]] -name = "babel" -version = "2.14.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, - {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, -] - -[package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] - -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "black" -version = "23.12.1" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, - {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, - {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, - {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, - {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, - {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, - {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, - {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, - {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, - {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, - {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, - {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, - {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, - {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, - {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, - {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, - {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, - {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, - {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, - {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, - {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, - {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2023.11.17" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, -] - -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "codecov" -version = "2.1.13" -description = "Hosted coverage reports for GitHub, Bitbucket and Gitlab" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5"}, - {file = "codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c"}, -] - -[package.dependencies] -coverage = "*" -requests = ">=2.7.9" - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.4.0" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, - {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, - {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, - {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, - {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, - {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, - {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, - {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, - {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, - {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, - {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, - {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, - {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, - {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "cryptography" -version = "42.0.4" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"}, - {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18"}, - {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2"}, - {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1"}, - {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b"}, - {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1"}, - {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992"}, - {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885"}, - {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824"}, - {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b"}, - {file = "cryptography-42.0.4-cp37-abi3-win32.whl", hash = "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925"}, - {file = "cryptography-42.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923"}, - {file = "cryptography-42.0.4-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7"}, - {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52"}, - {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a"}, - {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9"}, - {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764"}, - {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff"}, - {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257"}, - {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929"}, - {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0"}, - {file = "cryptography-42.0.4-cp39-abi3-win32.whl", hash = "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129"}, - {file = "cryptography-42.0.4-cp39-abi3-win_amd64.whl", hash = "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854"}, - {file = "cryptography-42.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298"}, - {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88"}, - {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20"}, - {file = "cryptography-42.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce"}, - {file = "cryptography-42.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74"}, - {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd"}, - {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b"}, - {file = "cryptography-42.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660"}, - {file = "cryptography-42.0.4.tar.gz", hash = "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb"}, -] - -[package.dependencies] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "distlib" -version = "0.3.8" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "docutils" -version = "0.18.1" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, - {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, -] - -[[package]] -name = "dparse" -version = "0.6.4b0" -description = "A parser for Python dependency files" -optional = false -python-versions = ">=3.7" -files = [ - {file = "dparse-0.6.4b0-py3-none-any.whl", hash = "sha256:592ff183348b8a5ea0a18442a7965e29445d3a26063654ec2c7e8ef42cd5753c"}, - {file = "dparse-0.6.4b0.tar.gz", hash = "sha256:f8d49b41a527f3d16a269f854e6665245b325e50e41d2c213810cb984553e5c8"}, -] - -[package.dependencies] -packaging = "*" -tomli = {version = "*", markers = "python_version < \"3.11\""} - -[package.extras] -all = ["dparse[conda]", "dparse[pipenv]", "dparse[poetry]"] -conda = ["pyyaml"] -pipenv = ["pipenv"] -poetry = ["poetry"] - -[[package]] -name = "exceptiongroup" -version = "1.2.0" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "filelock" -version = "3.13.1" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] - -[[package]] -name = "furo" -version = "2023.9.10" -description = "A clean customisable Sphinx documentation theme." -optional = false -python-versions = ">=3.8" -files = [ - {file = "furo-2023.9.10-py3-none-any.whl", hash = "sha256:513092538537dc5c596691da06e3c370714ec99bc438680edc1debffb73e5bfc"}, - {file = "furo-2023.9.10.tar.gz", hash = "sha256:5707530a476d2a63b8cad83b4f961f3739a69f4b058bcf38a03a39fa537195b2"}, -] - -[package.dependencies] -beautifulsoup4 = "*" -pygments = ">=2.7" -sphinx = ">=6.0,<8.0" -sphinx-basic-ng = "*" - -[[package]] -name = "identify" -version = "2.5.33" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, - {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.6" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, -] - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "7.0.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"}, - {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "jinja2" -version = "3.1.3" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "livereload" -version = "2.6.3" -description = "Python LiveReload is an awesome tool for web developers" -optional = false -python-versions = "*" -files = [ - {file = "livereload-2.6.3-py2.py3-none-any.whl", hash = "sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4"}, - {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, -] - -[package.dependencies] -six = "*" -tornado = {version = "*", markers = "python_version > \"2.7\""} - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "markupsafe" -version = "2.1.4" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, - {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, -] - -[[package]] -name = "marshmallow" -version = "3.20.2" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -optional = false -python-versions = ">=3.8" -files = [ - {file = "marshmallow-3.20.2-py3-none-any.whl", hash = "sha256:c21d4b98fee747c130e6bc8f45c4b3199ea66bc00c12ee1f639f0aeca034d5e9"}, - {file = "marshmallow-3.20.2.tar.gz", hash = "sha256:4c1daff273513dc5eb24b219a8035559dc573c8f322558ef85f5438ddd1236dd"}, -] - -[package.dependencies] -packaging = ">=17.0" - -[package.extras] -dev = ["pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"] -docs = ["alabaster (==0.7.15)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] -lint = ["pre-commit (>=2.4,<4.0)"] -tests = ["pytest", "pytz", "simplejson"] - -[[package]] -name = "mdit-py-plugins" -version = "0.4.0" -description = "Collection of plugins for markdown-it-py" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"}, - {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"}, -] - -[package.dependencies] -markdown-it-py = ">=1.0.0,<4.0.0" - -[package.extras] -code-style = ["pre-commit"] -rtd = ["myst-parser", "sphinx-book-theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mypy" -version = "1.8.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, - {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, - {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, - {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, - {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, - {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, - {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, - {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, - {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, - {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, - {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, - {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, - {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, - {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, - {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, - {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, - {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, - {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, - {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "myst-parser" -version = "2.0.0" -description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," -optional = false -python-versions = ">=3.8" -files = [ - {file = "myst_parser-2.0.0-py3-none-any.whl", hash = "sha256:7c36344ae39c8e740dad7fdabf5aa6fc4897a813083c6cc9990044eb93656b14"}, - {file = "myst_parser-2.0.0.tar.gz", hash = "sha256:ea929a67a6a0b1683cdbe19b8d2e724cd7643f8aa3e7bb18dd65beac3483bead"}, -] - -[package.dependencies] -docutils = ">=0.16,<0.21" -jinja2 = "*" -markdown-it-py = ">=3.0,<4.0" -mdit-py-plugins = ">=0.4,<1.0" -pyyaml = "*" -sphinx = ">=6,<8" - -[package.extras] -code-style = ["pre-commit (>=3.0,<4.0)"] -linkify = ["linkify-it-py (>=2.0,<3.0)"] -rtd = ["ipython", "pydata-sphinx-theme (==v0.13.0rc4)", "sphinx-autodoc2 (>=0.4.2,<0.5.0)", "sphinx-book-theme (==1.0.0rc2)", "sphinx-copybutton", "sphinx-design2", "sphinx-pyscript", "sphinx-tippy (>=0.3.1)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.8.2,<0.9.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] -testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=7,<8)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx-pytest"] -testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4,<0.4.0)"] - -[[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "numpy" -version = "1.26.3" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-1.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:806dd64230dbbfaca8a27faa64e2f414bf1c6622ab78cc4264f7f5f028fee3bf"}, - {file = "numpy-1.26.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f98011ba4ab17f46f80f7f8f1c291ee7d855fcef0a5a98db80767a468c85cd"}, - {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d45b3ec2faed4baca41c76617fcdcfa4f684ff7a151ce6fc78ad3b6e85af0a6"}, - {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdd2b45bf079d9ad90377048e2747a0c82351989a2165821f0c96831b4a2a54b"}, - {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:211ddd1e94817ed2d175b60b6374120244a4dd2287f4ece45d49228b4d529178"}, - {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1240f767f69d7c4c8a29adde2310b871153df9b26b5cb2b54a561ac85146485"}, - {file = "numpy-1.26.3-cp310-cp310-win32.whl", hash = "sha256:21a9484e75ad018974a2fdaa216524d64ed4212e418e0a551a2d83403b0531d3"}, - {file = "numpy-1.26.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e1591f6ae98bcfac2a4bbf9221c0b92ab49762228f38287f6eeb5f3f55905ce"}, - {file = "numpy-1.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b831295e5472954104ecb46cd98c08b98b49c69fdb7040483aff799a755a7374"}, - {file = "numpy-1.26.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e87562b91f68dd8b1c39149d0323b42e0082db7ddb8e934ab4c292094d575d6"}, - {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c66d6fec467e8c0f975818c1796d25c53521124b7cfb760114be0abad53a0a2"}, - {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f25e2811a9c932e43943a2615e65fc487a0b6b49218899e62e426e7f0a57eeda"}, - {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af36e0aa45e25c9f57bf684b1175e59ea05d9a7d3e8e87b7ae1a1da246f2767e"}, - {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:51c7f1b344f302067b02e0f5b5d2daa9ed4a721cf49f070280ac202738ea7f00"}, - {file = "numpy-1.26.3-cp311-cp311-win32.whl", hash = "sha256:7ca4f24341df071877849eb2034948459ce3a07915c2734f1abb4018d9c49d7b"}, - {file = "numpy-1.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:39763aee6dfdd4878032361b30b2b12593fb445ddb66bbac802e2113eb8a6ac4"}, - {file = "numpy-1.26.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7081fd19a6d573e1a05e600c82a1c421011db7935ed0d5c483e9dd96b99cf13"}, - {file = "numpy-1.26.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12c70ac274b32bc00c7f61b515126c9205323703abb99cd41836e8125ea0043e"}, - {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f784e13e598e9594750b2ef6729bcd5a47f6cfe4a12cca13def35e06d8163e3"}, - {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f24750ef94d56ce6e33e4019a8a4d68cfdb1ef661a52cdaee628a56d2437419"}, - {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:77810ef29e0fb1d289d225cabb9ee6cf4d11978a00bb99f7f8ec2132a84e0166"}, - {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8ed07a90f5450d99dad60d3799f9c03c6566709bd53b497eb9ccad9a55867f36"}, - {file = "numpy-1.26.3-cp312-cp312-win32.whl", hash = "sha256:f73497e8c38295aaa4741bdfa4fda1a5aedda5473074369eca10626835445511"}, - {file = "numpy-1.26.3-cp312-cp312-win_amd64.whl", hash = "sha256:da4b0c6c699a0ad73c810736303f7fbae483bcb012e38d7eb06a5e3b432c981b"}, - {file = "numpy-1.26.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1666f634cb3c80ccbd77ec97bc17337718f56d6658acf5d3b906ca03e90ce87f"}, - {file = "numpy-1.26.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18c3319a7d39b2c6a9e3bb75aab2304ab79a811ac0168a671a62e6346c29b03f"}, - {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b7e807d6888da0db6e7e75838444d62495e2b588b99e90dd80c3459594e857b"}, - {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4d362e17bcb0011738c2d83e0a65ea8ce627057b2fdda37678f4374a382a137"}, - {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b8c275f0ae90069496068c714387b4a0eba5d531aace269559ff2b43655edd58"}, - {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc0743f0302b94f397a4a65a660d4cd24267439eb16493fb3caad2e4389bccbb"}, - {file = "numpy-1.26.3-cp39-cp39-win32.whl", hash = "sha256:9bc6d1a7f8cedd519c4b7b1156d98e051b726bf160715b769106661d567b3f03"}, - {file = "numpy-1.26.3-cp39-cp39-win_amd64.whl", hash = "sha256:867e3644e208c8922a3be26fc6bbf112a035f50f0a86497f98f228c50c607bb2"}, - {file = "numpy-1.26.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3c67423b3703f8fbd90f5adaa37f85b5794d3366948efe9a5190a5f3a83fc34e"}, - {file = "numpy-1.26.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f47ee566d98849323f01b349d58f2557f02167ee301e5e28809a8c0e27a2d0"}, - {file = "numpy-1.26.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8474703bffc65ca15853d5fd4d06b18138ae90c17c8d12169968e998e448bb5"}, - {file = "numpy-1.26.3.tar.gz", hash = "sha256:697df43e2b6310ecc9d95f05d5ef20eacc09c7c4ecc9da3f235d39e71b7da1e4"}, -] - -[[package]] -name = "packaging" -version = "23.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, -] - -[[package]] -name = "paho-mqtt" -version = "1.6.1" -description = "MQTT version 5.0/3.1.1 client class" -optional = false -python-versions = "*" -files = [ - {file = "paho-mqtt-1.6.1.tar.gz", hash = "sha256:2a8291c81623aec00372b5a85558a372c747cbca8e9934dfe218638b8eefc26f"}, -] - -[package.extras] -proxy = ["PySocks"] - -[[package]] -name = "pandas-stubs" -version = "2.1.4.231227" -description = "Type annotations for pandas" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pandas_stubs-2.1.4.231227-py3-none-any.whl", hash = "sha256:211fc23e6ae87073bdf41dbf362c4a4d85e1e3477cb078dbac3da6c7fdaefba8"}, - {file = "pandas_stubs-2.1.4.231227.tar.gz", hash = "sha256:3ea29ef001e9e44985f5ebde02d4413f94891ef6ec7e5056fb07d125be796c23"}, -] - -[package.dependencies] -numpy = {version = ">=1.26.0", markers = "python_version < \"3.13\""} -types-pytz = ">=2022.1.1" - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.1.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, -] - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] - -[[package]] -name = "pluggy" -version = "1.3.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "3.6.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, - {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "pre-commit-hooks" -version = "4.5.0" -description = "Some out-of-the-box hooks for pre-commit." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pre_commit_hooks-4.5.0-py2.py3-none-any.whl", hash = "sha256:b779d5c44ede9b1fda48e2d96b08e9aa5b1d2fdb8903ca09f0dbaca22d529edb"}, - {file = "pre_commit_hooks-4.5.0.tar.gz", hash = "sha256:ffbe2af1c85ac9a7695866955680b4dee98822638b748a6f3debefad79748c8a"}, -] - -[package.dependencies] -"ruamel.yaml" = ">=0.15" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] - -[[package]] -name = "pydantic" -version = "1.10.14" -description = "Data validation and settings management using python type hints" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, - {file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, - {file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, - {file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, - {file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, - {file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, - {file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, - {file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, - {file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, - {file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, -] - -[package.dependencies] -typing-extensions = ">=4.2.0" - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] - -[[package]] -name = "pygments" -version = "2.17.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, -] - -[package.extras] -plugins = ["importlib-metadata"] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyserial" -version = "3.5" -description = "Python Serial Port Extension" -optional = false -python-versions = "*" -files = [ - {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, - {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, -] - -[package.extras] -cp2110 = ["hidapi"] - -[[package]] -name = "pytest" -version = "7.4.4" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.21.1" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, - {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] - -[[package]] -name = "pytest-cov" -version = "4.1.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "pytest-mock" -version = "3.12.0" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, - {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, -] - -[package.dependencies] -pytest = ">=5.0" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - -[[package]] -name = "pyupgrade" -version = "3.15.0" -description = "A tool to automatically upgrade syntax for newer versions." -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "pyupgrade-3.15.0-py2.py3-none-any.whl", hash = "sha256:8dc8ebfaed43566e2c65994162795017c7db11f531558a74bc8aa077907bc305"}, - {file = "pyupgrade-3.15.0.tar.gz", hash = "sha256:a7fde381060d7c224f55aef7a30fae5ac93bbc428367d27e70a603bc2acd4f00"}, -] - -[package.dependencies] -tokenize-rt = ">=5.2.0" - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "pyzmq" -version = "25.1.2" -description = "Python bindings for 0MQ" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:e624c789359f1a16f83f35e2c705d07663ff2b4d4479bad35621178d8f0f6ea4"}, - {file = "pyzmq-25.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49151b0efece79f6a79d41a461d78535356136ee70084a1c22532fc6383f4ad0"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9a5f194cf730f2b24d6af1f833c14c10f41023da46a7f736f48b6d35061e76e"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:faf79a302f834d9e8304fafdc11d0d042266667ac45209afa57e5efc998e3872"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f51a7b4ead28d3fca8dda53216314a553b0f7a91ee8fc46a72b402a78c3e43d"}, - {file = "pyzmq-25.1.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0ddd6d71d4ef17ba5a87becf7ddf01b371eaba553c603477679ae817a8d84d75"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:246747b88917e4867e2367b005fc8eefbb4a54b7db363d6c92f89d69abfff4b6"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:00c48ae2fd81e2a50c3485de1b9d5c7c57cd85dc8ec55683eac16846e57ac979"}, - {file = "pyzmq-25.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a68d491fc20762b630e5db2191dd07ff89834086740f70e978bb2ef2668be08"}, - {file = "pyzmq-25.1.2-cp310-cp310-win32.whl", hash = "sha256:09dfe949e83087da88c4a76767df04b22304a682d6154de2c572625c62ad6886"}, - {file = "pyzmq-25.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa99973d2ed20417744fca0073390ad65ce225b546febb0580358e36aa90dba6"}, - {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:82544e0e2d0c1811482d37eef297020a040c32e0687c1f6fc23a75b75db8062c"}, - {file = "pyzmq-25.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01171fc48542348cd1a360a4b6c3e7d8f46cdcf53a8d40f84db6707a6768acc1"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc69c96735ab501419c432110016329bf0dea8898ce16fab97c6d9106dc0b348"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e124e6b1dd3dfbeb695435dff0e383256655bb18082e094a8dd1f6293114642"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7598d2ba821caa37a0f9d54c25164a4fa351ce019d64d0b44b45540950458840"}, - {file = "pyzmq-25.1.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1299d7e964c13607efd148ca1f07dcbf27c3ab9e125d1d0ae1d580a1682399d"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4e6f689880d5ad87918430957297c975203a082d9a036cc426648fcbedae769b"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cc69949484171cc961e6ecd4a8911b9ce7a0d1f738fcae717177c231bf77437b"}, - {file = "pyzmq-25.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9880078f683466b7f567b8624bfc16cad65077be046b6e8abb53bed4eeb82dd3"}, - {file = "pyzmq-25.1.2-cp311-cp311-win32.whl", hash = "sha256:4e5837af3e5aaa99a091302df5ee001149baff06ad22b722d34e30df5f0d9097"}, - {file = "pyzmq-25.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:25c2dbb97d38b5ac9fd15586e048ec5eb1e38f3d47fe7d92167b0c77bb3584e9"}, - {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:11e70516688190e9c2db14fcf93c04192b02d457b582a1f6190b154691b4c93a"}, - {file = "pyzmq-25.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:313c3794d650d1fccaaab2df942af9f2c01d6217c846177cfcbc693c7410839e"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b3cbba2f47062b85fe0ef9de5b987612140a9ba3a9c6d2543c6dec9f7c2ab27"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc31baa0c32a2ca660784d5af3b9487e13b61b3032cb01a115fce6588e1bed30"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c9087b109070c5ab0b383079fa1b5f797f8d43e9a66c07a4b8b8bdecfd88ee"}, - {file = "pyzmq-25.1.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f8429b17cbb746c3e043cb986328da023657e79d5ed258b711c06a70c2ea7537"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5074adeacede5f810b7ef39607ee59d94e948b4fd954495bdb072f8c54558181"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7ae8f354b895cbd85212da245f1a5ad8159e7840e37d78b476bb4f4c3f32a9fe"}, - {file = "pyzmq-25.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b264bf2cc96b5bc43ce0e852be995e400376bd87ceb363822e2cb1964fcdc737"}, - {file = "pyzmq-25.1.2-cp312-cp312-win32.whl", hash = "sha256:02bbc1a87b76e04fd780b45e7f695471ae6de747769e540da909173d50ff8e2d"}, - {file = "pyzmq-25.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:ced111c2e81506abd1dc142e6cd7b68dd53747b3b7ae5edbea4578c5eeff96b7"}, - {file = "pyzmq-25.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7b6d09a8962a91151f0976008eb7b29b433a560fde056ec7a3db9ec8f1075438"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967668420f36878a3c9ecb5ab33c9d0ff8d054f9c0233d995a6d25b0e95e1b6b"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5edac3f57c7ddaacdb4d40f6ef2f9e299471fc38d112f4bc6d60ab9365445fb0"}, - {file = "pyzmq-25.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0dabfb10ef897f3b7e101cacba1437bd3a5032ee667b7ead32bbcdd1a8422fe7"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2c6441e0398c2baacfe5ba30c937d274cfc2dc5b55e82e3749e333aabffde561"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:16b726c1f6c2e7625706549f9dbe9b06004dfbec30dbed4bf50cbdfc73e5b32a"}, - {file = "pyzmq-25.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:a86c2dd76ef71a773e70551a07318b8e52379f58dafa7ae1e0a4be78efd1ff16"}, - {file = "pyzmq-25.1.2-cp36-cp36m-win32.whl", hash = "sha256:359f7f74b5d3c65dae137f33eb2bcfa7ad9ebefd1cab85c935f063f1dbb245cc"}, - {file = "pyzmq-25.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:55875492f820d0eb3417b51d96fea549cde77893ae3790fd25491c5754ea2f68"}, - {file = "pyzmq-25.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b8c8a419dfb02e91b453615c69568442e897aaf77561ee0064d789705ff37a92"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8807c87fa893527ae8a524c15fc505d9950d5e856f03dae5921b5e9aa3b8783b"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5e319ed7d6b8f5fad9b76daa0a68497bc6f129858ad956331a5835785761e003"}, - {file = "pyzmq-25.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3c53687dde4d9d473c587ae80cc328e5b102b517447456184b485587ebd18b62"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9add2e5b33d2cd765ad96d5eb734a5e795a0755f7fc49aa04f76d7ddda73fd70"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e690145a8c0c273c28d3b89d6fb32c45e0d9605b2293c10e650265bf5c11cfec"}, - {file = "pyzmq-25.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00a06faa7165634f0cac1abb27e54d7a0b3b44eb9994530b8ec73cf52e15353b"}, - {file = "pyzmq-25.1.2-cp37-cp37m-win32.whl", hash = "sha256:0f97bc2f1f13cb16905a5f3e1fbdf100e712d841482b2237484360f8bc4cb3d7"}, - {file = "pyzmq-25.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6cc0020b74b2e410287e5942e1e10886ff81ac77789eb20bec13f7ae681f0fdd"}, - {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:bef02cfcbded83473bdd86dd8d3729cd82b2e569b75844fb4ea08fee3c26ae41"}, - {file = "pyzmq-25.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e10a4b5a4b1192d74853cc71a5e9fd022594573926c2a3a4802020360aa719d8"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c5f80e578427d4695adac6fdf4370c14a2feafdc8cb35549c219b90652536ae"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5dde6751e857910c1339890f3524de74007958557593b9e7e8c5f01cd919f8a7"}, - {file = "pyzmq-25.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea1608dd169da230a0ad602d5b1ebd39807ac96cae1845c3ceed39af08a5c6df"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0f513130c4c361201da9bc69df25a086487250e16b5571ead521b31ff6b02220"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:019744b99da30330798bb37df33549d59d380c78e516e3bab9c9b84f87a9592f"}, - {file = "pyzmq-25.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e2713ef44be5d52dd8b8e2023d706bf66cb22072e97fc71b168e01d25192755"}, - {file = "pyzmq-25.1.2-cp38-cp38-win32.whl", hash = "sha256:07cd61a20a535524906595e09344505a9bd46f1da7a07e504b315d41cd42eb07"}, - {file = "pyzmq-25.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb7e49a17fb8c77d3119d41a4523e432eb0c6932187c37deb6fbb00cc3028088"}, - {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:94504ff66f278ab4b7e03e4cba7e7e400cb73bfa9d3d71f58d8972a8dc67e7a6"}, - {file = "pyzmq-25.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6dd0d50bbf9dca1d0bdea219ae6b40f713a3fb477c06ca3714f208fd69e16fd8"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:004ff469d21e86f0ef0369717351073e0e577428e514c47c8480770d5e24a565"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0b5ca88a8928147b7b1e2dfa09f3b6c256bc1135a1338536cbc9ea13d3b7add"}, - {file = "pyzmq-25.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9a79f1d2495b167119d02be7448bfba57fad2a4207c4f68abc0bab4b92925b"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:518efd91c3d8ac9f9b4f7dd0e2b7b8bf1a4fe82a308009016b07eaa48681af82"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1ec23bd7b3a893ae676d0e54ad47d18064e6c5ae1fadc2f195143fb27373f7f6"}, - {file = "pyzmq-25.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db36c27baed588a5a8346b971477b718fdc66cf5b80cbfbd914b4d6d355e44e2"}, - {file = "pyzmq-25.1.2-cp39-cp39-win32.whl", hash = "sha256:39b1067f13aba39d794a24761e385e2eddc26295826530a8c7b6c6c341584289"}, - {file = "pyzmq-25.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:8e9f3fabc445d0ce320ea2c59a75fe3ea591fdbdeebec5db6de530dd4b09412e"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a8c1d566344aee826b74e472e16edae0a02e2a044f14f7c24e123002dcff1c05"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759cfd391a0996345ba94b6a5110fca9c557ad4166d86a6e81ea526c376a01e8"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c61e346ac34b74028ede1c6b4bcecf649d69b707b3ff9dc0fab453821b04d1e"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cb8fc1f8d69b411b8ec0b5f1ffbcaf14c1db95b6bccea21d83610987435f1a4"}, - {file = "pyzmq-25.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3c00c9b7d1ca8165c610437ca0c92e7b5607b2f9076f4eb4b095c85d6e680a1d"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:df0c7a16ebb94452d2909b9a7b3337940e9a87a824c4fc1c7c36bb4404cb0cde"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45999e7f7ed5c390f2e87ece7f6c56bf979fb213550229e711e45ecc7d42ccb8"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac170e9e048b40c605358667aca3d94e98f604a18c44bdb4c102e67070f3ac9b"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1b604734bec94f05f81b360a272fc824334267426ae9905ff32dc2be433ab96"}, - {file = "pyzmq-25.1.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a793ac733e3d895d96f865f1806f160696422554e46d30105807fdc9841b9f7d"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0806175f2ae5ad4b835ecd87f5f85583316b69f17e97786f7443baaf54b9bb98"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef12e259e7bc317c7597d4f6ef59b97b913e162d83b421dd0db3d6410f17a244"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea253b368eb41116011add00f8d5726762320b1bda892f744c91997b65754d73"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b9b1f2ad6498445a941d9a4fee096d387fee436e45cc660e72e768d3d8ee611"}, - {file = "pyzmq-25.1.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8b14c75979ce932c53b79976a395cb2a8cd3aaf14aef75e8c2cb55a330b9b49d"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:889370d5174a741a62566c003ee8ddba4b04c3f09a97b8000092b7ca83ec9c49"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18fff090441a40ffda8a7f4f18f03dc56ae73f148f1832e109f9bffa85df15"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a6b36f95c98839ad98f8c553d8507644c880cf1e0a57fe5e3a3f3969040882"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4345c9a27f4310afbb9c01750e9461ff33d6fb74cd2456b107525bbeebcb5be3"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3516e0b6224cf6e43e341d56da15fd33bdc37fa0c06af4f029f7d7dfceceabbc"}, - {file = "pyzmq-25.1.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:146b9b1f29ead41255387fb07be56dc29639262c0f7344f570eecdcd8d683314"}, - {file = "pyzmq-25.1.2.tar.gz", hash = "sha256:93f1aa311e8bb912e34f004cf186407a4e90eec4f0ecc0efd26056bf7eda0226"}, -] - -[package.dependencies] -cffi = {version = "*", markers = "implementation_name == \"pypy\""} - -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.7" -files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rich" -version = "13.7.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "ruamel-yaml" -version = "0.18.5" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruamel.yaml-0.18.5-py3-none-any.whl", hash = "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada"}, - {file = "ruamel.yaml-0.18.5.tar.gz", hash = "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e"}, -] - -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} - -[package.extras] -docs = ["mercurial (>5.7)", "ryd"] -jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.8" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -optional = false -python-versions = ">=3.6" -files = [ - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, - {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, - {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, -] - -[[package]] -name = "ruff" -version = "0.0.292" -description = "An extremely fast Python linter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.0.292-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:02f29db018c9d474270c704e6c6b13b18ed0ecac82761e4fcf0faa3728430c96"}, - {file = "ruff-0.0.292-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69654e564342f507edfa09ee6897883ca76e331d4bbc3676d8a8403838e9fade"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c3c91859a9b845c33778f11902e7b26440d64b9d5110edd4e4fa1726c41e0a4"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4476f1243af2d8c29da5f235c13dca52177117935e1f9393f9d90f9833f69e4"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be8eb50eaf8648070b8e58ece8e69c9322d34afe367eec4210fdee9a555e4ca7"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9889bac18a0c07018aac75ef6c1e6511d8411724d67cb879103b01758e110a81"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bdfabd4334684a4418b99b3118793f2c13bb67bf1540a769d7816410402a205"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7c77c53bfcd75dbcd4d1f42d6cabf2485d2e1ee0678da850f08e1ab13081a8"}, - {file = "ruff-0.0.292-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e087b24d0d849c5c81516ec740bf4fd48bf363cfb104545464e0fca749b6af9"}, - {file = "ruff-0.0.292-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f160b5ec26be32362d0774964e218f3fcf0a7da299f7e220ef45ae9e3e67101a"}, - {file = "ruff-0.0.292-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ac153eee6dd4444501c4bb92bff866491d4bfb01ce26dd2fff7ca472c8df9ad0"}, - {file = "ruff-0.0.292-py3-none-musllinux_1_2_i686.whl", hash = "sha256:87616771e72820800b8faea82edd858324b29bb99a920d6aa3d3949dd3f88fb0"}, - {file = "ruff-0.0.292-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b76deb3bdbea2ef97db286cf953488745dd6424c122d275f05836c53f62d4016"}, - {file = "ruff-0.0.292-py3-none-win32.whl", hash = "sha256:e854b05408f7a8033a027e4b1c7f9889563dd2aca545d13d06711e5c39c3d003"}, - {file = "ruff-0.0.292-py3-none-win_amd64.whl", hash = "sha256:f27282bedfd04d4c3492e5c3398360c9d86a295be00eccc63914438b4ac8a83c"}, - {file = "ruff-0.0.292-py3-none-win_arm64.whl", hash = "sha256:7f67a69c8f12fbc8daf6ae6d36705037bde315abf8b82b6e1f4c9e74eb750f68"}, - {file = "ruff-0.0.292.tar.gz", hash = "sha256:1093449e37dd1e9b813798f6ad70932b57cf614e5c2b5c51005bf67d55db33ac"}, -] - -[[package]] -name = "safety" -version = "3.0.1" -description = "Checks installed dependencies for known vulnerabilities and licenses." -optional = false -python-versions = ">=3.7" -files = [ - {file = "safety-3.0.1-py3-none-any.whl", hash = "sha256:1ed058bc4bef132b974e58d7fcad020fb897cd255328016f8a5a194b94ca91d2"}, - {file = "safety-3.0.1.tar.gz", hash = "sha256:1f2000f03652f3a0bfc67f8fd1e98bc5723ccb76e15cb1bdd68545c3d803df01"}, -] - -[package.dependencies] -Authlib = ">=1.2.0" -Click = ">=8.0.2" -dparse = ">=0.6.4b0" -jinja2 = ">=3.1.0" -marshmallow = ">=3.15.0" -packaging = ">=21.0" -pydantic = ">=1.10.12,<2.0" -requests = "*" -rich = "*" -"ruamel.yaml" = ">=0.17.21" -safety-schemas = ">=0.0.1" -setuptools = ">=65.5.1" -typer = "*" -typing-extensions = ">=4.7.1" -urllib3 = ">=1.26.5" - -[package.extras] -github = ["pygithub (>=1.43.3)"] -gitlab = ["python-gitlab (>=1.3.0)"] -spdx = ["spdx-tools (>=0.8.2)"] - -[[package]] -name = "safety-schemas" -version = "0.0.1" -description = "Schemas for Safety CLI" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "safety_schemas-0.0.1-py3-none-any.whl", hash = "sha256:33ba340a726036e1063fe075d93af88e9cddd067a1a1d294b7ebabfbe52028df"}, - {file = "safety_schemas-0.0.1.tar.gz", hash = "sha256:de56d04a9badbbab8b360326d1a598d68b180b766eb04d3296abaee4c7ab431c"}, -] - -[package.dependencies] -dparse = ">=0.6.2" -packaging = ">=21.0,<=23.0" -pydantic = ">=1.10.12,<2.0.0" -ruamel-yaml = ">=0.17.21" -typing-extensions = ">=4.7.1,<5.0.0" - -[[package]] -name = "setuptools" -version = "69.0.3" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, - {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - -[[package]] -name = "soupsieve" -version = "2.5" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, -] - -[[package]] -name = "sphinx" -version = "7.2.6" -description = "Python documentation generator" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinx-7.2.6-py3-none-any.whl", hash = "sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560"}, - {file = "sphinx-7.2.6.tar.gz", hash = "sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5"}, -] - -[package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=2.9" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.18.1,<0.21" -imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.0" -packaging = ">=21.0" -Pygments = ">=2.14" -requests = ">=2.25.0" -snowballstemmer = ">=2.0" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.9" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] -test = ["cython (>=3.0)", "filelock", "html5lib", "pytest (>=4.6)", "setuptools (>=67.0)"] - -[[package]] -name = "sphinx-autobuild" -version = "2021.3.14" -description = "Rebuild Sphinx documentation on changes, with live-reload in the browser." -optional = false -python-versions = ">=3.6" -files = [ - {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, - {file = "sphinx_autobuild-2021.3.14-py3-none-any.whl", hash = "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac"}, -] - -[package.dependencies] -colorama = "*" -livereload = "*" -sphinx = "*" - -[package.extras] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "1.25.2" -description = "Type hints (PEP 484) support for the Sphinx autodoc extension" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinx_autodoc_typehints-1.25.2-py3-none-any.whl", hash = "sha256:5ed05017d23ad4b937eab3bee9fae9ab0dd63f0b42aa360031f1fad47e47f673"}, - {file = "sphinx_autodoc_typehints-1.25.2.tar.gz", hash = "sha256:3cabc2537e17989b2f92e64a399425c4c8bf561ed73f087bc7414a5003616a50"}, -] - -[package.dependencies] -sphinx = ">=7.1.2" - -[package.extras] -docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)"] -numpy = ["nptyping (>=2.5)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.7.1)"] - -[[package]] -name = "sphinx-basic-ng" -version = "1.0.0b2" -description = "A modern skeleton for Sphinx themes." -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, - {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, -] - -[package.dependencies] -sphinx = ">=4.0" - -[package.extras] -docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] - -[[package]] -name = "sphinx-rtd-theme" -version = "1.3.0" -description = "Read the Docs theme for Sphinx" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, - {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, -] - -[package.dependencies] -docutils = "<0.19" -sphinx = ">=1.6,<8" -sphinxcontrib-jquery = ">=4,<5" - -[package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.8" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, - {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.6" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, - {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.5" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, - {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -standalone = ["Sphinx (>=5)"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -description = "Extension to include jQuery on newer Sphinx releases" -optional = false -python-versions = ">=2.7" -files = [ - {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, - {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, -] - -[package.dependencies] -Sphinx = ">=1.8" - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.7" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, - {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.10" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, - {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, -] - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "termcolor" -version = "2.4.0" -description = "ANSI color formatting for output in terminal" -optional = false -python-versions = ">=3.8" -files = [ - {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, - {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, -] - -[package.extras] -tests = ["pytest", "pytest-cov"] - -[[package]] -name = "tokenize-rt" -version = "5.2.0" -description = "A wrapper around the stdlib `tokenize` which roundtrips." -optional = false -python-versions = ">=3.8" -files = [ - {file = "tokenize_rt-5.2.0-py2.py3-none-any.whl", hash = "sha256:b79d41a65cfec71285433511b50271b05da3584a1da144a0752e9c621a285289"}, - {file = "tokenize_rt-5.2.0.tar.gz", hash = "sha256:9fe80f8a5c1edad2d3ede0f37481cc0cc1538a2f442c9c2f9e4feacd2792d054"}, -] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tornado" -version = "6.4" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -optional = false -python-versions = ">= 3.8" -files = [ - {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, - {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, - {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, - {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, - {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, -] - -[[package]] -name = "typeguard" -version = "4.1.5" -description = "Run-time type checker for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typeguard-4.1.5-py3-none-any.whl", hash = "sha256:8923e55f8873caec136c892c3bed1f676eae7be57cdb94819281b3d3bc9c0953"}, - {file = "typeguard-4.1.5.tar.gz", hash = "sha256:ea0a113bbc111bcffc90789ebb215625c963411f7096a7e9062d4e4630c155fd"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -typing-extensions = {version = ">=4.7.0", markers = "python_version < \"3.12\""} - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=7)", "mypy (>=1.2.0)", "pytest (>=7)"] - -[[package]] -name = "typer" -version = "0.9.0" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.6" -files = [ - {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"}, - {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"}, -] - -[package.dependencies] -click = ">=7.1.1,<9.0.0" -typing-extensions = ">=3.7.4.3" - -[package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] -test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"] - -[[package]] -name = "types-paho-mqtt" -version = "1.6.0.20240106" -description = "Typing stubs for paho-mqtt" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-paho-mqtt-1.6.0.20240106.tar.gz", hash = "sha256:6b486b9e4438c856cc0ac8312bf6a81c2bca1697bf36cde6d2ecddb44513550e"}, - {file = "types_paho_mqtt-1.6.0.20240106-py3-none-any.whl", hash = "sha256:1d233c2c017a512ebbec24d6a90d94302767c75a33a7c2584a660eac7fade248"}, -] - -[[package]] -name = "types-pytz" -version = "2023.3.1.1" -description = "Typing stubs for pytz" -optional = false -python-versions = "*" -files = [ - {file = "types-pytz-2023.3.1.1.tar.gz", hash = "sha256:cc23d0192cd49c8f6bba44ee0c81e4586a8f30204970fc0894d209a6b08dab9a"}, - {file = "types_pytz-2023.3.1.1-py3-none-any.whl", hash = "sha256:1999a123a3dc0e39a2ef6d19f3f8584211de9e6a77fe7a0259f04a524e90a5cf"}, -] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.12" -description = "Typing stubs for PyYAML" -optional = false -python-versions = "*" -files = [ - {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"}, - {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"}, -] - -[[package]] -name = "typing-extensions" -version = "4.9.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, -] - -[[package]] -name = "urllib3" -version = "2.1.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, - {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "virtualenv" -version = "20.25.0" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, - {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "xdoctest" -version = "1.1.2" -description = "A rewrite of the builtin doctest module" -optional = false -python-versions = ">=3.6" -files = [ - {file = "xdoctest-1.1.2-py3-none-any.whl", hash = "sha256:ebe133222534f09597cbe461f97cc5f95ad7b36e5d31f3437caffb9baaddbddb"}, - {file = "xdoctest-1.1.2.tar.gz", hash = "sha256:267d3d4e362547fa917d3deabaf6888232bbf43c8d30298faeb957dbfa7e0ba3"}, -] - -[package.dependencies] -colorama = {version = "*", optional = true, markers = "platform_system == \"Windows\" and extra == \"colors\""} -Pygments = {version = "*", optional = true, markers = "python_version >= \"3.5.0\" and extra == \"colors\""} - -[package.extras] -all = ["IPython (>=7.10.0)", "IPython (>=7.23.1)", "Pygments (>=2.0.0)", "Pygments (>=2.4.1)", "attrs (>=19.2.0)", "colorama (>=0.4.1)", "debugpy (>=1.0.0)", "debugpy (>=1.0.0)", "debugpy (>=1.0.0)", "debugpy (>=1.3.0)", "debugpy (>=1.6.0)", "ipykernel (>=5.2.0)", "ipykernel (>=6.0.0)", "ipykernel (>=6.11.0)", "ipython-genutils (>=0.2.0)", "jedi (>=0.16)", "jinja2 (>=3.0.0)", "jupyter-client (>=6.1.5)", "jupyter-client (>=7.0.0)", "jupyter-core (>=4.7.0)", "nbconvert (>=6.0.0)", "nbconvert (>=6.1.0)", "pyflakes (>=2.2.0)", "pytest (>=4.6.0)", "pytest (>=4.6.0)", "pytest (>=6.2.5)", "pytest-cov (>=3.0.0)", "tomli (>=0.2.0)", "typing (>=3.7.4)"] -all-strict = ["IPython (==7.10.0)", "IPython (==7.23.1)", "Pygments (==2.0.0)", "Pygments (==2.4.1)", "attrs (==19.2.0)", "colorama (==0.4.1)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==5.2.0)", "ipykernel (==6.0.0)", "ipykernel (==6.11.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==6.1.5)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "nbconvert (==6.1.0)", "pyflakes (==2.2.0)", "pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "tomli (==0.2.0)", "typing (==3.7.4)"] -colors = ["Pygments", "Pygments", "colorama"] -jupyter = ["IPython", "IPython", "attrs", "debugpy", "debugpy", "debugpy", "debugpy", "debugpy", "ipykernel", "ipykernel", "ipykernel", "ipython-genutils", "jedi", "jinja2", "jupyter-client", "jupyter-client", "jupyter-core", "nbconvert", "nbconvert"] -optional = ["IPython (>=7.10.0)", "IPython (>=7.23.1)", "Pygments (>=2.0.0)", "Pygments (>=2.4.1)", "attrs (>=19.2.0)", "colorama (>=0.4.1)", "debugpy (>=1.0.0)", "debugpy (>=1.0.0)", "debugpy (>=1.0.0)", "debugpy (>=1.3.0)", "debugpy (>=1.6.0)", "ipykernel (>=5.2.0)", "ipykernel (>=6.0.0)", "ipykernel (>=6.11.0)", "ipython-genutils (>=0.2.0)", "jedi (>=0.16)", "jinja2 (>=3.0.0)", "jupyter-client (>=6.1.5)", "jupyter-client (>=7.0.0)", "jupyter-core (>=4.7.0)", "nbconvert (>=6.0.0)", "nbconvert (>=6.1.0)", "pyflakes (>=2.2.0)", "tomli (>=0.2.0)"] -optional-strict = ["IPython (==7.10.0)", "IPython (==7.23.1)", "Pygments (==2.0.0)", "Pygments (==2.4.1)", "attrs (==19.2.0)", "colorama (==0.4.1)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.0.0)", "debugpy (==1.3.0)", "debugpy (==1.6.0)", "ipykernel (==5.2.0)", "ipykernel (==6.0.0)", "ipykernel (==6.11.0)", "ipython-genutils (==0.2.0)", "jedi (==0.16)", "jinja2 (==3.0.0)", "jupyter-client (==6.1.5)", "jupyter-client (==7.0.0)", "jupyter-core (==4.7.0)", "nbconvert (==6.0.0)", "nbconvert (==6.1.0)", "pyflakes (==2.2.0)", "tomli (==0.2.0)"] -tests = ["pytest (>=4.6.0)", "pytest (>=4.6.0)", "pytest (>=6.2.5)", "pytest-cov (>=3.0.0)", "typing (>=3.7.4)"] -tests-binary = ["cmake", "cmake", "ninja", "ninja", "pybind11", "pybind11", "scikit-build", "scikit-build"] -tests-binary-strict = ["cmake (==3.21.2)", "cmake (==3.25.0)", "ninja (==1.10.2)", "ninja (==1.11.1)", "pybind11 (==2.10.3)", "pybind11 (==2.7.1)", "scikit-build (==0.11.1)", "scikit-build (==0.16.1)"] -tests-strict = ["pytest (==4.6.0)", "pytest (==4.6.0)", "pytest (==6.2.5)", "pytest-cov (==3.0.0)", "typing (==3.7.4)"] - -[[package]] -name = "zipp" -version = "3.17.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.9" -content-hash = "58477d507211576d135f6bfa2eb07d2d0967225e862f3192cd61a14e0c7dbe56" diff --git a/pyproject.toml b/pyproject.toml index 43e7673..ec0ead9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,57 +1,66 @@ -[tool.poetry] +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] name = "heisskleber" -version = "0.5.7" description = "Heisskleber" -authors = ["Felix Weiler "] -license = "MIT" +authors = [ + { name = "Felix Weiler-Detjen", email = "felix@flucto.tech" }, +] +license = {file = "LICENSE"} readme = "README.md" -homepage = "https://github.com/flucto-gmbh/heisskleber" -repository = "https://github.com/flucto-gmbh/heisskleber" -documentation = "https://heisskleber.readthedocs.io" -[tool.poetry.urls] -Changelog = "https://github.com/flucto-gmbh/heisskleber/releases" +requires-python = ">=3.10" +dynamic = ["version"] +dependencies= [ + "aiomqtt>=2.3.0", + "pyserial>=3.5", + "pyyaml>=6.0.2", + "pyzmq>=26.2.0", +] -[tool.poetry.dependencies] -python = "^3.9" -paho-mqtt = "^1.6.1" -pyserial = "^3.5" -pyyaml = "^6.0.1" -pyzmq = "^25.1.1" -aiomqtt = "^1.2.1" - -[tool.poetry.group.dev.dependencies] -black = ">=21.10b0" -coverage = { extras = ["toml"], version = ">=6.2" } -furo = ">=2021.11.12" -mypy = ">=0.930" -pre-commit = ">=2.16.0" -pre-commit-hooks = ">=4.1.0" -pytest = ">=6.2.5" -pytest-cov = "^4.1.0" -pytest-mock = "^3.11.1" -ruff = "^0.0.292" -pyupgrade = ">=2.29.1" -safety = ">=1.10.3" -sphinx = ">=4.3.2" -sphinx-autobuild = ">=2021.3.14" -sphinx-autodoc-typehints = "^1.24.0" -sphinx-rtd-theme = "^1.3.0" -typeguard = ">=2.13.3" -xdoctest = { extras = ["colors"], version = ">=0.15.10" } -myst-parser = { version = ">=0.16.1" } -pytest-asyncio = "^0.21.1" -termcolor = "^2.4.0" -codecov = "^2.1.13" +[project.urls] +Homepage = "https://github.com/flucto-gmbh/heisskleber" +Repository = "https://github.com/flucto-gmbh/heisskleber" +Documentation = "https://heisskleber.readthedocs.io" -[tool.poetry.group.types.dependencies] -pandas-stubs = "^2.1.1.230928" -types-pyyaml = "^6.0.12.12" -types-paho-mqtt = "^1.6.0.7" -[tool.poetry.scripts] -hkcli = "heisskleber.run.cli:main" -zmqbroker = "heisskleber.run.zmqbroker:main" +[project.optional-dependencies] +test = [ + "pytest>=8.3.3", + "pytest-cov>=5.0.0", + "coverage[toml]>=7.6.1", + "xdoctest>=1.2.0", + "pytest-asyncio>=0.24.0", +] +docs = [ + "furo>=2024.8.6", + "myst-parser>=4.0.0", + "sphinx>=8.0.2", + "sphinx-autobuild>=2024.9.19", + "sphinx-rtd-theme>=0.5.1", + "sphinx_copybutton", + "sphinx_autodoc_typehints", +] +filter = [ + "numpy>=2.1.1", + "scipy>=1.14.1", +] + +[tool.uv] +dev-dependencies = [ + "deptry>=0.20.0", + "mypy>=1.11.2", + "ruff>=0.6.8", + "xdoctest>=1.2.0", + "nox>=2024.4.15", + "pytest>=8.3.3", + "pytest-cov>=5.0.0", + "coverage[toml]>=7.6.1", + "pytest-asyncio>=0.24.0", +] +package = true [tool.coverage.paths] source = ["heisskleber", "*/site-packages"] @@ -59,7 +68,7 @@ tests = ["tests", "*/tests"] [tool.coverage.run] branch = true -source = ["heisskleber"] +source = ["src/heisskleber"] omit = ["tests/*"] [tool.coverage.report] @@ -75,39 +84,48 @@ show_error_context = true exclude = ["tests/*", "^test_*\\.py"] [tool.ruff] -ignore-init-module-imports = true target-version = "py39" line-length = 120 fix = true -select = [ - "YTT", # flake8-2020 - "S", # flake8-bandit - "B", # flake8-bugbear - "A", # flake8-builtins - "C4", # flake8-comprehensions - "T10", # flake8-debugger - "SIM", # flake8-simplify - "I", # isort - "C90", # mccabe - "E", - "W", # pycodestyle - "F", # pyflakes - "PGH", # pygrep-hooks - "UP", # pyupgrade - "RUF", # ruff - "TRY", # tryceratops -] + +[tool.ruff.lint] +select = ["ALL"] ignore = [ "E501", # LineTooLong "E731", # DoNotAssignLambda "A001", # - "PGH003", # Use specific rules when ignoring type issues + "D100", # Missing module docstring + "D104", # Missing package docstring + "D107", # Missing __init__ docstring + "ANN101", # Deprecated and stupid self annotation + "ANN401", # Dynamically typed annotation + "FA102", # Missing from __future__ import annotations + "FBT001", # boolean style argument in function definition + "FBT002", # boolean style argument in function definition + "ARG002", # Unused kwargs + "TD002", + "TD003", + "FIX002", + "COM812", + "ISC001", + "ARG001", + "INP001" ] -[tool.ruff.per-file-ignores] -"tests/*" = ["S101", "D"] -"tests/test_import.py" = ["F401"] +[tool.ruff.lint.pydocstyle] +convention = "google" -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["S101", "D", "T201", "PLR2", "SLF001", "ANN"] +"bin/*" = [ + "ERA001", # Found commented-out code +] + + +[tool.hatch] +version.source = "vcs" +version.path = "src/heisskleber/__init__.py" + +[tool.hatch.envs.default] +features = ["test"] +scripts.test = "pytest {args}" diff --git a/run/export_configs.py b/run/export_configs.py deleted file mode 100644 index 1df10b8..0000000 --- a/run/export_configs.py +++ /dev/null @@ -1,17 +0,0 @@ -from dataclasses import asdict - -import yaml - -from heisskleber.mqtt.config import MqttConf -from heisskleber.serial.config import SerialConf -from heisskleber.tcp.config import TcpConf -from heisskleber.udp.config import UdpConf -from heisskleber.zmq.config import ZmqConf - -configs = {"mqtt": MqttConf(), "zmq": ZmqConf(), "udp": UdpConf(), "tcp": TcpConf(), "serial": SerialConf()} - - -for name, config in configs.items(): - with open(f"./config/heisskleber/{name}.yaml", "w") as file: - file.write(f"# Heisskleber config file for {config.__class__.__name__}\n") - file.write(yaml.dump(asdict(config))) diff --git a/run/udp-listener.py b/run/udp-listener.py deleted file mode 100644 index 9e5e0fe..0000000 --- a/run/udp-listener.py +++ /dev/null @@ -1,14 +0,0 @@ -from heisskleber.udp import UdpConf, UdpSubscriber - - -def main() -> None: - conf = UdpConf(host="192.168.137.1", port=6600) - subscriber = UdpSubscriber(conf) - - while True: - topic, data = subscriber.receive() - print(topic, data) - - -if __name__ == "__main__": - main() diff --git a/src/heisskleber/__init__.py b/src/heisskleber/__init__.py new file mode 100644 index 0000000..1f06a4b --- /dev/null +++ b/src/heisskleber/__init__.py @@ -0,0 +1,38 @@ +"""Heisskleber.""" + +from heisskleber.console import ConsoleReceiver, ConsoleSender +from heisskleber.core import Receiver, Sender +from heisskleber.mqtt import MqttConf, MqttReceiver, MqttSender +from heisskleber.serial import SerialConf, SerialReceiver, SerialSender +from heisskleber.tcp import TcpConf, TcpReceiver, TcpSender +from heisskleber.udp import UdpConf, UdpReceiver, UdpSender +from heisskleber.zmq import ZmqConf, ZmqReceiver, ZmqSender + +__all__ = [ + "Sender", + "Receiver", + # mqtt + "MqttConf", + "MqttSender", + "MqttReceiver", + # zmq + "ZmqConf", + "ZmqSender", + "ZmqReceiver", + # udp + "UdpConf", + "UdpSender", + "UdpReceiver", + # tcp + "TcpConf", + "TcpSender", + "TcpReceiver", + # serial + "SerialConf", + "SerialSender", + "SerialReceiver", + # console + "ConsoleSender", + "ConsoleReceiver", +] +__version__ = "1.0.0" diff --git a/src/heisskleber/console/__init__.py b/src/heisskleber/console/__init__.py new file mode 100644 index 0000000..e5ce6a5 --- /dev/null +++ b/src/heisskleber/console/__init__.py @@ -0,0 +1,4 @@ +from heisskleber.console.receiver import ConsoleReceiver +from heisskleber.console.sender import ConsoleSender + +__all__ = ["ConsoleReceiver", "ConsoleSender"] diff --git a/src/heisskleber/console/receiver.py b/src/heisskleber/console/receiver.py new file mode 100644 index 0000000..e928e73 --- /dev/null +++ b/src/heisskleber/console/receiver.py @@ -0,0 +1,46 @@ +import asyncio +import sys +from typing import Any, TypeVar + +from heisskleber.core import Receiver, Unpacker, json_unpacker + +T = TypeVar("T") + + +class ConsoleReceiver(Receiver[T]): + """Read stdin from console and create data of type T.""" + + def __init__( + self, + unpacker: Unpacker[T] = json_unpacker, # type: ignore[assignment] + ) -> None: + self.queue: asyncio.Queue[tuple[T, dict[str, Any]]] = asyncio.Queue(maxsize=10) + self.unpack = unpacker + self.task: asyncio.Task[None] | None = None + + async def _listener_task(self) -> None: + while True: + payload = sys.stdin.readline().encode() # I know this is stupid, but I adhere to the interface for now + data, extra = self.unpack(payload) + await self.queue.put((data, extra)) + + async def receive(self) -> tuple[T, dict[str, Any]]: + """Receive the next message from the console input.""" + if not self.task: + self.task = asyncio.create_task(self._listener_task()) + + data, extra = await self.queue.get() + return data, extra + + def __repr__(self) -> str: + """Return string representation of ConsoleSource.""" + return f"{self.__class__.__name__}" + + async def start(self) -> None: + """Start ConsoleSource.""" + self.task = asyncio.create_task(self._listener_task()) + + async def stop(self) -> None: + """Stop ConsoleSource.""" + if self.task: + self.task.cancel() diff --git a/src/heisskleber/console/sender.py b/src/heisskleber/console/sender.py new file mode 100644 index 0000000..c234f5e --- /dev/null +++ b/src/heisskleber/console/sender.py @@ -0,0 +1,35 @@ +from typing import Any, TypeVar + +from heisskleber.core import Packer, Sender, json_packer + +T = TypeVar("T") + + +class ConsoleSender(Sender[T]): + """Send data to console out.""" + + def __init__( + self, + pretty: bool = False, + verbose: bool = False, + packer: Packer[T] = json_packer, # type: ignore[assignment] + ) -> None: + self.verbose = verbose + self.pretty = pretty + self.packer = packer + + async def send(self, data: T, topic: str | None = None, **kwargs: dict[str, Any]) -> None: + """Serialize data and write to console output.""" + serialized = self.packer(data) + output = f"{topic}:\t{serialized.decode()}" if topic else serialized.decode() + print(output) # noqa: T201 + + def __repr__(self) -> str: + """Return string reprensentation of ConsoleSink.""" + return f"{self.__class__.__name__}(pretty={self.pretty}, verbose={self.verbose})" + + async def start(self) -> None: + """Not implemented.""" + + async def stop(self) -> None: + """Not implemented.""" diff --git a/src/heisskleber/core/__init__.py b/src/heisskleber/core/__init__.py new file mode 100644 index 0000000..95ae358 --- /dev/null +++ b/src/heisskleber/core/__init__.py @@ -0,0 +1,23 @@ +"""Core classes of the heisskleber library.""" + +from .config import BaseConf, ConfigType +from .packer import JSONPacker, Packer, PackerError +from .receiver import Receiver +from .sender import Sender +from .unpacker import JSONUnpacker, Unpacker, UnpackError + +json_packer = JSONPacker() +json_unpacker = JSONUnpacker() + +__all__ = [ + "Packer", + "Unpacker", + "Sender", + "Receiver", + "json_packer", + "json_unpacker", + "BaseConf", + "ConfigType", + "PackerError", + "UnpackError", +] diff --git a/src/heisskleber/core/config.py b/src/heisskleber/core/config.py new file mode 100644 index 0000000..04ac42c --- /dev/null +++ b/src/heisskleber/core/config.py @@ -0,0 +1,127 @@ +"""Configuration baseclass.""" + +import logging +from dataclasses import dataclass, fields +from pathlib import Path +from typing import Any, TextIO, TypeVar, Union + +import yaml # type: ignore[import-untyped] + +logger = logging.getLogger("heisskleber") + +ConfigType = TypeVar( + "ConfigType", + bound="BaseConf", +) # https://stackoverflow.com/a/46227137 , https://docs.python.org/3/library/typing.html#typing.TypeVar + + +def _parse_yaml(file: TextIO) -> dict[str, Any]: + try: + return dict(yaml.safe_load(file)) + except yaml.YAMLError as e: + msg = "Failed to parse config file!" + logger.exception(msg) + raise ValueError(msg) from e + + +def _parse_json(file: TextIO) -> dict[str, Any]: + import json + + try: + return dict(json.load(file)) + except json.JSONDecodeError as e: + msg = "Failed to parse config file!" + logger.exception(msg) + raise ValueError(msg) from e + + +def _parser(path: Path) -> dict[str, Any]: + suffix = path.suffix.lower() + + with path.open() as f: + if suffix in [".yaml", ".yml"]: + return _parse_yaml(f) + if suffix == ".json": + return _parse_json(f) + msg = f"Unsupported file format {suffix}." + logger.exception(msg) + raise ValueError + + +@dataclass +class BaseConf: + """Default configuration class for generic configuration info.""" + + def __post_init__(self) -> None: + """Check if all attributes are the same type as the original defition of the dataclass.""" + for field in fields(self): + value = getattr(self, field.name) + if value is None: # Allow optional fields + continue + if not isinstance(value, field.type): # Failed field comparison + raise TypeError + if ( # Failed Union comparison + hasattr(field.type, "__origin__") + and field.type.__origin__ is Union + and not any(isinstance(value, t) for t in field.type.__args__) + ): + raise TypeError + + @classmethod + def from_dict(cls: type[ConfigType], config_dict: dict[str, Any]) -> ConfigType: + """Create a config instance from a dictionary, including only fields defined in the dataclass. + + Arguments: + config_dict: Dictionary containing configuration values. + Keys should match dataclass field names. + + Returns: + An instance of the configuration class with values from the dictionary. + + Raises: + TypeError: If provided values don't match field types. + + Example: + >>> from dataclasses import dataclass + >>> @dataclass + ... class ServerConfig(BaseConf): + ... host: str + ... port: int + ... debug: bool = False + >>> + >>> config = ServerConfig.from_dict({ + ... "host": "localhost", + ... "port": 8080, + ... "debug": True, + ... "invalid_key": "ignored" # Will be filtered out + ... }) + >>> config.host + 'localhost' + >>> config.port + 8080 + >>> config.debug + True + >>> hasattr(config, "invalid_key") # Extra keys are ignored + False + >>> + >>> # Type validation + >>> try: + ... ServerConfig.from_dict({"host": "localhost", "port": "8080"}) # Wrong type + ... except TypeError as e: + ... print("TypeError raised as expected") + TypeError raised as expected + + """ + valid_fields = {f.name for f in fields(cls)} + filtered_dict = {k: v for k, v in config_dict.items() if k in valid_fields} + return cls(**filtered_dict) + + @classmethod + def from_file(cls: type[ConfigType], file_path: str | Path) -> ConfigType: + """Create a config instance from a file - accepts yaml or json.""" + path = Path(file_path) + if not path.exists(): + logger.exception("Config file not found: %(path)s", {"path": path}) + raise FileNotFoundError + + return cls.from_dict(_parser(path)) diff --git a/src/heisskleber/core/packer.py b/src/heisskleber/core/packer.py new file mode 100644 index 0000000..3ea010e --- /dev/null +++ b/src/heisskleber/core/packer.py @@ -0,0 +1,80 @@ +"""Packer and unpacker for network data.""" + +import json +from abc import abstractmethod +from typing import Any, Protocol, TypeVar + +T_contra = TypeVar("T_contra", contravariant=True) + + +class PackerError(Exception): + """Raised when unpacking operations fail. + + This exception wraps underlying errors that may occur during unpacking, + providing a consistent interface for error handling. + + Arguments: + data: The data object that caused the PackerError + + """ + + PREVIEW_LENGTH = 100 + + def __init__(self, data: Any) -> None: + """Initialize the error with the failed payload and cause.""" + message = "Failed to pack data." + super().__init__(message) + + +class Packer(Protocol[T_contra]): + """Packer Interface. + + This class defines a protocol for packing data. + It takes data and converts it into a bytes payload. + + Attributes: + None + + """ + + @abstractmethod + def __call__(self, data: T_contra) -> bytes: + """Packs the data dictionary into a bytes payload. + + Arguments: + data (T_contra): The input data dictionary to be packed. + + Returns: + bytes: The packed payload. + + Raises: + PackerError: The data dictionary could not be packed. + + """ + + +class JSONPacker(Packer[dict[str, Any]]): + """Converts a dictionary into JSON-formatted bytes. + + Arguments: + data: A dictionary with string keys and arbitrary values to be serialized into JSON format. + + Returns: + bytes: The JSON-encoded data as a bytes object. + + Raises: + PackerError: If the data cannot be serialized to JSON. + + Example: + >>> packer = JSONPacker() + >>> result = packer({"key": "value"}) + b'{"key": "value"}' + + """ + + def __call__(self, data: dict[str, Any]) -> bytes: + """Pack the data.""" + try: + return json.dumps(data).encode() + except (UnicodeEncodeError, TypeError) as err: + raise PackerError(data) from err diff --git a/src/heisskleber/core/receiver.py b/src/heisskleber/core/receiver.py new file mode 100644 index 0000000..5d821bc --- /dev/null +++ b/src/heisskleber/core/receiver.py @@ -0,0 +1,101 @@ +"""Asynchronous data source interface.""" + +from abc import ABC, abstractmethod +from collections.abc import AsyncGenerator +from types import TracebackType +from typing import Any, Generic + +from .unpacker import T_co, Unpacker + + +class Receiver(ABC, Generic[T_co]): + """Abstract interface for asynchronous data sources. + + This class defines a protocol for receiving data from various input streams + asynchronously. It supports both async iteration and context manager patterns, + and ensures proper resource management. + + The source is covariant in its type parameter, allowing for type-safe subtyping + relationships. + + Attributes: + unpacker: Component responsible for deserializing incoming data into type T_co. + + Example: + >>> async with CustomSource(unpacker) as source: + ... async for data, metadata in source: + ... print(f"Received: {data}, metadata: {metadata}") + + """ + + unpacker: Unpacker[T_co] + + @abstractmethod + async def receive(self) -> tuple[T_co, dict[str, Any]]: + """Receive data from the implemented input stream. + + Returns: + tuple[T_co, dict[str, Any]]: A tuple containing: + - The received and unpacked data of type T_co + - A dictionary of metadata associated with the received data + + Raises: + Any implementation-specific exceptions that might occur during receiving. + + """ + + @abstractmethod + async def start(self) -> None: + """Initialize and start any background processes and tasks of the source.""" + + @abstractmethod + async def stop(self) -> None: + """Stop any background processes and tasks.""" + + @abstractmethod + def __repr__(self) -> str: + """A string reprensatiion of the source.""" + + async def __aiter__(self) -> AsyncGenerator[tuple[T_co, dict[str, Any]], None]: + """Implement async iteration over the source's data stream. + + Yields: + tuple[T_co, dict[str, Any]]: Each data item and its associated metadata + as returned by receive(). + + Raises: + Any exceptions that might occur during receive(). + + """ + while True: + data, meta = await self.receive() + yield data, meta + + async def __aenter__(self) -> "Receiver[T_co]": + """Initialize the source for use in an async context manager. + + Returns: + AsyncSource[T_co]: The initialized source instance. + + Raises: + Any exceptions that might occur during start(). + + """ + await self.start() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Cleanup the source when exiting an async context manager. + + Arguments: + exc_type: The type of the exception that was raised, if any. + exc_value: The instance of the exception that was raised, if any. + traceback: The traceback of the exception that was raised, if any. + + """ + await self.stop() diff --git a/src/heisskleber/core/sender.py b/src/heisskleber/core/sender.py new file mode 100644 index 0000000..ab4d07b --- /dev/null +++ b/src/heisskleber/core/sender.py @@ -0,0 +1,79 @@ +"""Asyncronous data sink interface.""" + +from abc import ABC, abstractmethod +from types import TracebackType +from typing import Any, Generic, TypeVar + +from .packer import Packer + +T = TypeVar("T") + + +class Sender(ABC, Generic[T]): + """Abstract interface for asynchronous data sinks. + + This class defines a protocol for sending data to various output streams + asynchronously. It supports context manager usage and ensures proper + resource management. + + Attributes: + packer: Component responsible for serializing type T data before sending. + + """ + + packer: Packer[T] + + @abstractmethod + async def send(self, data: T, **kwargs: Any) -> None: + """Send data through the implemented output stream. + + Arguments: + data: The data to be sent, of type T. + **kwargs: Additional implementation-specific arguments. + + """ + + @abstractmethod + async def start(self) -> None: + """Initialize and start the sink's background processes and tasks.""" + + @abstractmethod + async def stop(self) -> None: + """Stop and cleanup the sink's background processes and tasks. + + This method should be called when the sink is no longer needed. + It should handle cleanup of any resources initialized in start(). + """ + + @abstractmethod + def __repr__(self) -> str: + """A string representation of the sink.""" + + async def __aenter__(self) -> "Sender[T]": + """Initialize the sink for use in an async context manager. + + Returns: + AsyncSink[T]: The initialized sink instance. + + Raises: + Any exceptions that might occur during start(). + + """ + await self.start() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Cleanup the sink when exiting an async context manager. + + Arguments: + exc_type: The type of the exception that was raised, if any. + exc_value: The instance of the exception that was raised, if any. + traceback: The traceback of the exception that was raised, if any. + + """ + await self.stop() diff --git a/src/heisskleber/core/unpacker.py b/src/heisskleber/core/unpacker.py new file mode 100644 index 0000000..198a68d --- /dev/null +++ b/src/heisskleber/core/unpacker.py @@ -0,0 +1,84 @@ +"""Unpacker protocol definition and example implemetation.""" + +import json +from abc import abstractmethod +from typing import Any, Protocol, TypeVar + +T_co = TypeVar("T_co", covariant=True) + + +class UnpackError(Exception): + """Raised when unpacking operations fail. + + This exception wraps underlying errors that may occur during unpacking, + providing a consistent interface for error handling. + + Arguments: + payload: The bytes payload that failed to unpack. + + """ + + PREVIEW_LENGTH = 100 + + def __init__(self, payload: bytes) -> None: + """Initialize the error with the failed payload and cause.""" + self.payload = payload + preview = payload[: self.PREVIEW_LENGTH] + b"..." if len(payload) > self.PREVIEW_LENGTH else payload + message = f"Failed to unpack payload: {preview!r}. " + super().__init__(message) + + +class Unpacker(Protocol[T_co]): + """Unpacker Interface. + + This abstract base class defines an interface for unpacking payloads. + It takes a payload of bytes, creates a data dictionary and an optional topic, + and returns a tuple containing the topic and data. + """ + + @abstractmethod + def __call__(self, payload: bytes) -> tuple[T_co, dict[str, Any]]: + """Unpacks the payload into a data object and optional meta-data dictionary. + + Args: + payload (bytes): The input payload to be unpacked. + + Returns: + tuple[T, Optional[dict[str, Any]]]: A tuple containing: + - T: The data object generated from the input data, e.g. dict or dataclass + - dict[str, Any]: The meta data associated with the unpack operation, such as topic, timestamp or errors + + Raises: + UnpackError: The payload could not be unpacked. + + """ + + +class JSONUnpacker(Unpacker[dict[str, Any]]): + """Deserializes JSON-formatted bytes into dictionaries. + + Arguments: + payload: JSON-formatted bytes to deserialize. + + Returns: + tuple[dict[str, Any], dict[str, Any]]: A tuple containing: + - The deserialized JSON data as a dictionary + - An empty dictionary for metadata (not used in JSON unpacking) + + Raises: + UnpackError: If the payload cannot be decoded as valid JSON. + + Example: + >>> unpacker = JSONUnpacker() + >>> data, metadata = unpacker(b'{"hotglue": "very_nais"}') + >>> print(data) + {'hotglue': 'very_nais'} + + """ + + def __call__(self, payload: bytes) -> tuple[dict[str, Any], dict[str, Any]]: + """Unpack the payload.""" + try: + return json.loads(payload), {} + except json.JSONDecodeError as e: + raise UnpackError(payload) from e diff --git a/src/heisskleber/core/utils.py b/src/heisskleber/core/utils.py new file mode 100644 index 0000000..c42418b --- /dev/null +++ b/src/heisskleber/core/utils.py @@ -0,0 +1,44 @@ +import asyncio +from collections.abc import Coroutine +from functools import wraps +from typing import Any, Callable, ParamSpec, TypeVar + +P = ParamSpec("P") +T = TypeVar("T") + + +def retry( + every: int = 5, + strategy: str = "always", + catch: type[Exception] | tuple[type[Exception], ...] = Exception, + logger_fn: Callable[[str, dict[str, Any]], None] | None = None, +) -> Callable[[Callable[P, Coroutine[Any, Any, T]]], Callable[P, Coroutine[Any, Any, T]]]: + """Retry a coroutine.""" + + def decorator(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, Coroutine[Any, Any, T]]: + @wraps(func) + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: + retries = 0 + while True: + try: + result = await func(*args, **kwargs) + break + except catch as e: + if logger_fn: + logger_fn( + "Error occurred: %(err). Retrying in %(seconds) seconds", + {"err": e, "seconds": every}, + ) + retries += 1 + await asyncio.sleep(every) + except asyncio.CancelledError: + raise + + if strategy != "always": + raise NotImplementedError + + return result + + return wrapper + + return decorator diff --git a/src/heisskleber/mqtt/__init__.py b/src/heisskleber/mqtt/__init__.py new file mode 100644 index 0000000..abd26f9 --- /dev/null +++ b/src/heisskleber/mqtt/__init__.py @@ -0,0 +1,13 @@ +"""Async wrappers for mqtt functionality. + +MQTT implementation is achieved via the `aiomqtt`_ package, which is an async wrapper around the `paho-mqtt`_ package. + +.. _aiomqtt: https://github.com/mossblaser/aiomqtt +.. _paho-mqtt: https://github.com/eclipse/paho.mqtt.python +""" + +from .config import MqttConf +from .receiver import MqttReceiver +from .sender import MqttSender + +__all__ = ["MqttConf", "MqttReceiver", "MqttSender"] diff --git a/src/heisskleber/mqtt/config.py b/src/heisskleber/mqtt/config.py new file mode 100644 index 0000000..f639618 --- /dev/null +++ b/src/heisskleber/mqtt/config.py @@ -0,0 +1,50 @@ +"""Mqtt config.""" + +from dataclasses import dataclass +from typing import Any + +from aiomqtt import Will + +from heisskleber.core import BaseConf + + +@dataclass +class WillConf(BaseConf): + """MQTT Last Will and Testament message configuration.""" + + topic: str + payload: str | None = None + qos: int = 0 + retain: bool = False + + def to_aiomqtt_will(self) -> Will: + """Create an aiomqtt style will.""" + return Will(topic=self.topic, payload=self.payload, qos=self.qos, retain=self.retain, properties=None) + + +@dataclass +class MqttConf(BaseConf): + """MQTT configuration class.""" + + # transport + host: str = "localhost" + port: int = 1883 + ssl: bool = False + + # mqtt + user: str = "" + password: str = "" + qos: int = 0 + retain: bool = False + max_saved_messages: int = 100 + timeout: int = 60 + keep_alive: int = 60 + will: Will | None = None + + @classmethod + def from_dict(cls, config_dict: dict[str, Any]) -> "MqttConf": + """Create a MqttConf object from a dictionary.""" + if "will" in config_dict: + config_dict = config_dict.copy() + config_dict["will"] = WillConf.from_dict(config_dict["will"]).to_aiomqtt_will() + return super().from_dict(config_dict) diff --git a/src/heisskleber/mqtt/receiver.py b/src/heisskleber/mqtt/receiver.py new file mode 100644 index 0000000..1e0b890 --- /dev/null +++ b/src/heisskleber/mqtt/receiver.py @@ -0,0 +1,133 @@ +import asyncio +import logging +from asyncio import Queue, Task, create_task +from typing import Any, TypeVar + +from aiomqtt import Client, Message, MqttError + +from heisskleber.core import Receiver, Unpacker, json_unpacker +from heisskleber.core.utils import retry +from heisskleber.mqtt import MqttConf + +T = TypeVar("T") +logger = logging.getLogger("heisskleber.mqtt") + + +class MqttReceiver(Receiver[T]): + """Asynchronous MQTT subscriber based on aiomqtt. + + This class implements an asynchronous MQTT subscriber that handles connection, subscription, and message reception from an MQTT broker. It uses aiomqtt as the underlying MQTT client implementation. + + The subscriber maintains a queue of received messages which can be accessed through the `receive` method. + + Attributes: + config (MqttConf): Stored configuration for MQTT connection. + topics (Union[str, List[str]]): Topics to subscribe to. + + """ + + def __init__( + self, + config: MqttConf, + topic: str | list[str], + unpacker: Unpacker[T] = json_unpacker, # type: ignore[assignment] + ) -> None: + """Initialize the MQTT source. + + Args: + config: Configuration object containing: + - host (str): MQTT broker hostname + - port (int): MQTT broker port + - user (str): Username for authentication + - password (str): Password for authentication + - qos (int): Default Quality of Service level + - max_saved_messages (int): Maximum queue size + topic: Single topic string or list of topics to subscribe to + unpacker: Function to deserialize received messages, defaults to json_unpacker + + """ + self.config = config + self.topics = topic if isinstance(topic, list) else [topic] + self.unpacker = unpacker + self._message_queue: Queue[Message] = Queue(self.config.max_saved_messages) + self._listener_task: Task[None] | None = None + + async def receive(self) -> tuple[T, dict[str, Any]]: + """Receive and process the next message from the queue. + + Returns: + tuple[T, dict[str, Any]] + - The unpacked message data + - A dictionary with metadata including the message topic + + Raises: + TypeError: If the message payload is not of type bytes. + UnpackError: If the message could not be unpacked with the unpacker protocol. + + """ + if not self._listener_task: + await self.start() + + message = await self._message_queue.get() + if not isinstance(message.payload, bytes): + error_msg = "Payload is not of type bytes." + raise TypeError(error_msg) + + data, extra = self.unpacker(message.payload) + extra["topic"] = message.topic.value + return (data, extra) + + def __repr__(self) -> str: + """Return string representation of Mqtt Source class.""" + return f"{self.__class__.__name__}(broker={self.config.host}, port={self.config.port})" + + async def start(self) -> None: + """Start the MQTT listener task.""" + self._listener_task = create_task(self._run()) + + async def stop(self) -> None: + """Stop the MQTT listener task.""" + if not self._listener_task: + return + + self._listener_task.cancel() + try: + await self._listener_task + except asyncio.CancelledError: + # Raise if the stop task was cancelled + # kudos:https://superfastpython.com/asyncio-cancel-task-and-wait/ + task = asyncio.current_task() + if task and task.cancelled(): + raise + self._listener_task = None + + async def subscribe(self, topic: str, qos: int | None = None) -> None: + """Subscribe to an additional MQTT topic. + + Args: + topic: The topic to subscribe to + qos: Quality of Service level, uses config.qos if None + + """ + qos = qos or self.config.qos + self.topics.append(topic) + await self._client.subscribe(topic, qos) + + @retry(every=1, catch=MqttError, logger_fn=logger.exception) + async def _run(self) -> None: + """Background task for MQTT connection.""" + async with Client( + hostname=self.config.host, + port=self.config.port, + username=self.config.user, + password=self.config.password, + timeout=self.config.timeout, + keepalive=self.config.keep_alive, + will=self.config.will, + ) as client: + self._client = client + logger.info("subscribing to %(topics)s", {"topics": self.topics}) + await client.subscribe([(topic, self.config.qos) for topic in self.topics]) + + async for message in client.messages: + await self._message_queue.put(message) diff --git a/src/heisskleber/mqtt/sender.py b/src/heisskleber/mqtt/sender.py new file mode 100644 index 0000000..5cdc8c2 --- /dev/null +++ b/src/heisskleber/mqtt/sender.py @@ -0,0 +1,99 @@ +"""Async mqtt sink implementation.""" + +import asyncio +import logging +from asyncio import CancelledError, create_task +from typing import Any, TypeVar + +import aiomqtt + +from heisskleber.core import Packer, Sender, json_packer +from heisskleber.core.utils import retry + +from .config import MqttConf + +T = TypeVar("T") + +logger = logging.getLogger("heisskleber.mqtt") + + +class MqttSender(Sender[T]): + """MQTT publisher with queued message handling. + + This sink implementation provides asynchronous MQTT publishing capabilities with automatic connection management and message queueing. + Network operations are handled in a separate task. + + Attributes: + config: MQTT configuration in a dataclass. + packer: Callable to pack data from type T to bytes for transport. + + """ + + def __init__(self, config: MqttConf, packer: Packer[T] = json_packer) -> None: # type: ignore[assignment] + self.config = config + self.packer = packer + self._send_queue: asyncio.Queue[tuple[T, str]] = asyncio.Queue() + self._sender_task: asyncio.Task[None] | None = None + + async def send(self, data: T, topic: str = "mqtt", qos: int = 0, retain: bool = False, **kwargs: Any) -> None: + """Queue data for asynchronous publication to the mqtt broker. + + Arguments: + data: The data to be published. + topic: The mqtt topic to publish to. + qos: MQTT QOS level (0, 1, or 2). Defaults to 0.o + retain: Whether to set the MQTT retain flag. Defaults to False. + **kwargs: Not implemented. + + """ + if not self._sender_task: + await self.start() + + await self._send_queue.put((data, topic)) + + @retry(every=5, catch=aiomqtt.MqttError, logger_fn=logger.exception) + async def _send_work(self) -> None: + async with aiomqtt.Client( + hostname=self.config.host, + port=self.config.port, + username=self.config.user, + password=self.config.password, + timeout=float(self.config.timeout), + keepalive=self.config.keep_alive, + will=self.config.will, + ) as client: + try: + while True: + data, topic = await self._send_queue.get() + payload = self.packer(data) + await client.publish(topic=topic, payload=payload) + except CancelledError: + logger.info("MqttSink background loop cancelled. Emptying queue...") + while not self._send_queue.empty(): + _ = self._send_queue.get_nowait() + raise + + def __repr__(self) -> str: + """Return string representation of the MQTT sink object.""" + return f"{self.__class__.__name__}(broker={self.config.host}, port={self.config.port})" + + async def start(self) -> None: + """Start the send queue in a separate task. + + The task will retry connections every 5 seconds on failure. + """ + self._sender_task = create_task(self._send_work()) + + async def stop(self) -> None: + """Stop the background task.""" + if not self._sender_task: + return + self._sender_task.cancel() + try: + await self._sender_task + except asyncio.CancelledError: + # If the stop task was cancelled, we raise. + task = asyncio.current_task() + if task and task.cancelled(): + raise + self._sender_task = None diff --git a/heisskleber/py.typed b/src/heisskleber/py.typed similarity index 100% rename from heisskleber/py.typed rename to src/heisskleber/py.typed diff --git a/src/heisskleber/serial/__init__.py b/src/heisskleber/serial/__init__.py new file mode 100644 index 0000000..ed4feb2 --- /dev/null +++ b/src/heisskleber/serial/__init__.py @@ -0,0 +1,7 @@ +"""Asyncronous implementations to read and write to a serial interface.""" + +from .config import SerialConf +from .receiver import SerialReceiver +from .sender import SerialSender + +__all__ = ["SerialConf", "SerialSender", "SerialReceiver"] diff --git a/src/heisskleber/serial/config.py b/src/heisskleber/serial/config.py new file mode 100644 index 0000000..15167e5 --- /dev/null +++ b/src/heisskleber/serial/config.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +from typing import Literal + +from heisskleber.core.config import BaseConf + + +@dataclass +class SerialConf(BaseConf): + """Serial Config class. + + Attributes: + port: The port to connect to. Defaults to /dev/serial0. + baudrate: The baudrate of the serial connection. Defaults to 9600. + bytesize: The bytesize of the messages. Defaults to 8. + encoding: The string encoding of the messages. Defaults to ascii. + parity: The parity checking value. One of "N" for none, "E" for even, "O" for odd. Defaults to None. + stopbits: Stopbits. One of 1, 2 or 1.5. Defaults to 1. + + Note: + stopbits 1.5 is not yet implemented. + + """ + + port: str = "/dev/serial0" + baudrate: int = 9600 + bytesize: int = 8 + encoding: str = "ascii" + parity: Literal["N", "O", "E"] = "N" # definitions from serial.PARTITY_'N'ONE / 'O'DD / 'E'VEN + stopbits: Literal[1, 2] = 1 # 1.5 not yet implemented diff --git a/src/heisskleber/serial/receiver.py b/src/heisskleber/serial/receiver.py new file mode 100644 index 0000000..a4332a9 --- /dev/null +++ b/src/heisskleber/serial/receiver.py @@ -0,0 +1,98 @@ +import asyncio +import logging +from concurrent.futures import ThreadPoolExecutor +from typing import Any, TypeVar + +import serial # type: ignore[import-untyped] + +from heisskleber.core import Receiver, Unpacker + +from .config import SerialConf + +T = TypeVar("T") +logger = logging.getLogger("heisskleber.serial") + + +class SerialReceiver(Receiver[T]): + """An asynchronous source for reading data from a serial port. + + This class implements the AsyncSource interface for reading data from a serial port. + It uses a thread pool executor to perform blocking I/O operations asynchronously. + + Attributes: + config: Configuration for the serial port. + unpacker: Function to unpack received data. + + """ + + def __init__(self, config: SerialConf, unpack: Unpacker[T]) -> None: + self.config = config + self.unpacker = unpack + self._loop = asyncio.get_running_loop() + self._executor = ThreadPoolExecutor(max_workers=2) + self._lock = asyncio.Lock() + self._is_connected = False + self._cancel_read_timeout = 1 + + async def receive(self) -> tuple[T, dict[str, Any]]: + """Receive data from the serial port. + + This method reads a line from the serial port, unpacks it, and returns the data. + If the serial port is not connected, it will attempt to connect first. + + Returns: + tuple[T, dict[str, Any]]: A tuple containing the unpacked data and any extra information. + + Raises: + UnpackError: If the data could not be unpacked with the provided unpacker. + + """ + if not self._is_connected: + await self.start() + + try: + payload = await asyncio.get_running_loop().run_in_executor(self._executor, self._ser.readline, -1) + except asyncio.CancelledError: + await asyncio.shield(self._cancel_read()) + raise + + data, extra = self.unpacker(payload=payload) + logger.debug( + "SerialSource(%(port)s): Unpacked: %(data)s, extra information: %(extra)s", + {"port": self.config.port, "data": data, "extra": extra}, + ) + return (data, extra) + + async def _cancel_read(self) -> None: + if not hasattr(self, "_ser"): + return + logger.warning( + "SerialSource(%(port)s).read() cancelled, waiting for %(timeout)s", + {"port": self.config.port, "timeout": self._cancel_read_timeout}, + ) + await asyncio.wait_for( + asyncio.get_running_loop().run_in_executor(self._executor, self._ser.cancel_read), + self._cancel_read_timeout, + ) + + async def start(self) -> None: + """Open serial device.""" + if hasattr(self, "_ser"): + return + + self._ser = serial.Serial( + port=self.config.port, + baudrate=self.config.baudrate, + bytesize=self.config.bytesize, + parity=self.config.parity, + stopbits=self.config.stopbits, + ) + + async def stop(self) -> None: + """Close serial connection.""" + await self._cancel_read() + self._ser.close() + + def __repr__(self) -> str: + """Return string representation of Serial Source.""" + return f"SerialSource({self.config.port}, baudrate={self.config.baudrate})" diff --git a/src/heisskleber/serial/sender.py b/src/heisskleber/serial/sender.py new file mode 100644 index 0000000..f433bdd --- /dev/null +++ b/src/heisskleber/serial/sender.py @@ -0,0 +1,91 @@ +"""Asynchronous sink implementation for sending data via serial port.""" + +import asyncio +from concurrent.futures import ThreadPoolExecutor +from typing import Any, TypeVar + +import serial # type: ignore[import-untyped] + +from heisskleber.core import Packer, Sender + +from .config import SerialConf + +T = TypeVar("T") + + +class SerialSender(Sender[T]): + """An asynchronous sink for writing data to a serial port. + + This class implements the AsyncSink interface for writing data to a serial port. + It uses a thread pool executor to perform blocking I/O operations asynchronously. + + Attributes: + config: Configuration for the serial port. + packer: Function to pack data for sending. + """ + + def __init__(self, config: SerialConf, pack: Packer[T]) -> None: + """SerialSink constructor.""" + self.config = config + self.packer = pack + self._loop = asyncio.get_running_loop() + self._executor = ThreadPoolExecutor(max_workers=2) + self._lock = asyncio.Lock() + self._is_connected = False + self._cancel_write_timeout = 1 + + async def send(self, data: T, **kwargs: dict[str, Any]) -> None: + """Send data to the serial port. + + This method packs the data, writes it to the serial port, and then flushes the port. + + Arguments: + data: The data to be sent. + **kwargs: Not implemented. + + Raises: + PackerError: If data could not be packed to bytes with the provided packer. + + Note: + If the serial port is not connected, it will implicitly attempt to connect first. + + """ + if not self._is_connected: + await self.start() + + payload = self.packer(data) + try: + await asyncio.get_running_loop().run_in_executor(self._executor, self._ser.write, payload) + await asyncio.get_running_loop().run_in_executor(self._executor, self._ser.flush) + except asyncio.CancelledError: + await asyncio.shield(self._cancel_write()) + raise + + async def _cancel_write(self) -> None: + if not hasattr(self, "_ser"): + return + await asyncio.wait_for( + asyncio.get_running_loop().run_in_executor(self._executor, self._ser.cancel_write), + self._cancel_write_timeout, + ) + + async def start(self) -> None: + """Open serial connection.""" + if hasattr(self, "_ser"): + return + + self._ser = serial.Serial( + port=self.config.port, + baudrate=self.config.baudrate, + bytesize=self.config.bytesize, + parity=self.config.parity, + stopbits=self.config.stopbits, + ) + + async def stop(self) -> None: + """Close serial connection.""" + self._ser.close() + + def __repr__(self) -> str: + """Return string representation of SerialSink.""" + return f"SerialSink({self.config.port}, baudrate={self.config.baudrate})" diff --git a/src/heisskleber/tcp/__init__.py b/src/heisskleber/tcp/__init__.py new file mode 100644 index 0000000..bc72952 --- /dev/null +++ b/src/heisskleber/tcp/__init__.py @@ -0,0 +1,5 @@ +from .config import TcpConf +from .receiver import TcpReceiver +from .sender import TcpSender + +__all__ = ["TcpReceiver", "TcpSender", "TcpConf"] diff --git a/src/heisskleber/tcp/config.py b/src/heisskleber/tcp/config.py new file mode 100644 index 0000000..2c32e53 --- /dev/null +++ b/src/heisskleber/tcp/config.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass +from enum import Enum + +from heisskleber.core import BaseConf + + +@dataclass +class TcpConf(BaseConf): + """Configuration dataclass for TCP connections.""" + + class RestartBehavior(Enum): + """The three types of restart behaviour.""" + + NEVER = 0 # Never restart on failure + ONCE = 1 # Restart once + ALWAYS = 2 # Restart until the connection succeeds + + host: str = "localhost" + port: int = 6000 + timeout: int = 60 + retry_delay: float = 0.5 + restart_behavior: RestartBehavior = RestartBehavior.ALWAYS diff --git a/src/heisskleber/tcp/receiver.py b/src/heisskleber/tcp/receiver.py new file mode 100644 index 0000000..07dae80 --- /dev/null +++ b/src/heisskleber/tcp/receiver.py @@ -0,0 +1,107 @@ +"""Async TCP Source - get data from arbitrary TCP server.""" + +import asyncio +import logging +from typing import Any, TypeVar + +from heisskleber.core import Receiver, Unpacker, json_unpacker +from heisskleber.tcp.config import TcpConf + +T = TypeVar("T") + +logger = logging.getLogger("heisskleber.tcp") + + +class TcpReceiver(Receiver[T]): + """Async TCP connection, connects to host:port and reads byte encoded strings.""" + + def __init__(self, config: TcpConf, unpacker: Unpacker[T] = json_unpacker) -> None: # type: ignore [assignment] + self.config = config + self.unpack = unpacker + self.is_connected = False + self.timeout = config.timeout + self._start_task: asyncio.Task[None] | None = None + self.reader: asyncio.StreamReader | None = None + self.writer: asyncio.StreamWriter | None = None + + async def receive(self) -> tuple[T, dict[str, Any]]: + """Receive data from a connection. + + Attempt to read data from the connection and handle the process of re-establishing the connection if necessary. + + Returns: + tuple[T, dict[str, Any]] + - The unpacked message data + - A dictionary with metadata including the message topic + + Raises: + TypeError: If the message payload is not of type bytes. + UnpackError: If the message could not be unpacked with the unpacker protocol. + + """ + data = b"" + retry_delay = self.config.retry_delay + while not data: + await self._ensure_connected() + data = await self.reader.readline() # type: ignore [union-attr] + if not data: + self.is_connected = False + logger.warning( + "%(self)s nothing received, retrying connect in %(seconds)s", + {"self": self, "seconds": retry_delay}, + ) + await asyncio.sleep(retry_delay) + retry_delay = min(self.config.timeout, retry_delay * 2) + + payload, extra = self.unpack(data) + return payload, extra + + async def start(self) -> None: + """Start TcpSource.""" + await self._connect() + + async def stop(self) -> None: + """Stop TcpSource.""" + if self.is_connected: + logger.info("%(self)s stopping", {"self": self}) + + async def _ensure_connected(self) -> None: + if self.is_connected: + return + + # Not connected, try to (re-)connect + if not self._start_task: + # Possibly multiple reconnects, so can't just await once + self._start_task = asyncio.create_task(self._connect()) + + try: + await self._start_task + finally: + self._start_task = None + + async def _connect(self) -> None: + logger.info("%(self)s waiting for connection.", {"self": self}) + + num_attempts = 0 + while True: + try: + self.reader, self.writer = await asyncio.wait_for( + asyncio.open_connection(self.config.host, self.config.port), + timeout=self.timeout, + ) + logger.info("%(self)s connected successfully!", {"self": self}) + break + except ConnectionRefusedError as e: + logger.exception("%(self)s: %(error_type)s", {"self": self, "error_type": type(e).__name__}) + if self.config.restart_behavior == TcpConf.RestartBehavior.NEVER: + raise + num_attempts += 1 + if self.config.restart_behavior == TcpConf.RestartBehavior.ONCE and num_attempts > 1: + raise + # otherwise retry indefinitely + + self.is_connected = True + + def __repr__(self) -> str: + """Return string representation of TcpSource.""" + return f"{self.__class__.__name__}(host={self.config.host}, port={self.config.port})" diff --git a/src/heisskleber/tcp/sender.py b/src/heisskleber/tcp/sender.py new file mode 100644 index 0000000..d0de16e --- /dev/null +++ b/src/heisskleber/tcp/sender.py @@ -0,0 +1,41 @@ +from typing import Any, TypeVar + +from heisskleber.core import Sender +from heisskleber.core.packer import Packer + +from .config import TcpConf + +T = TypeVar("T") + + +class TcpSender(Sender[T]): + """Async TCP Sink. + + Attributes: + config: The TcpConf configuration object. + packer: The packer protocol to serialize data before sending. + + """ + + def __init__(self, config: TcpConf, packer: Packer[T]) -> None: + self.config = config + self.packer = packer + + async def send(self, data: T, **kwargs: dict[str, Any]) -> None: + """Send data via tcp connection. + + Arguments: + data: The data to be sent. + kwargs: Not implemented. + + """ + + def __repr__(self) -> str: + """Return string representation of TcpSink.""" + return f"TcpSink({self.config.host}:{self.config.port})" + + async def start(self) -> None: + """Start TcpSink.""" + + async def stop(self) -> None: + """Stop TcpSink.""" diff --git a/src/heisskleber/udp/__init__.py b/src/heisskleber/udp/__init__.py new file mode 100644 index 0000000..dd984b1 --- /dev/null +++ b/src/heisskleber/udp/__init__.py @@ -0,0 +1,5 @@ +from .config import UdpConf +from .receiver import UdpReceiver +from .sender import UdpSender + +__all__ = ["UdpReceiver", "UdpSender", "UdpConf"] diff --git a/heisskleber/udp/config.py b/src/heisskleber/udp/config.py similarity index 66% rename from heisskleber/udp/config.py rename to src/heisskleber/udp/config.py index 5ff60b4..8e6c84e 100644 --- a/heisskleber/udp/config.py +++ b/src/heisskleber/udp/config.py @@ -1,17 +1,14 @@ from dataclasses import dataclass -from heisskleber.config import BaseConf +from heisskleber.core import BaseConf @dataclass class UdpConf(BaseConf): - """ - UDP configuration. - """ + """UDP configuration.""" port: int = 1234 host: str = "127.0.0.1" - packer: str = "json" max_queue_size: int = 1000 encoding: str = "utf-8" delimiter: str = "\r\n" diff --git a/src/heisskleber/udp/receiver.py b/src/heisskleber/udp/receiver.py new file mode 100644 index 0000000..0c61b8c --- /dev/null +++ b/src/heisskleber/udp/receiver.py @@ -0,0 +1,86 @@ +import asyncio +import logging +from typing import Any, TypeVar + +from heisskleber.core import Receiver, Unpacker, json_unpacker +from heisskleber.udp.config import UdpConf + +logger = logging.getLogger("heisskleber.udp") +T = TypeVar("T") + + +class UdpProtocol(asyncio.DatagramProtocol): + """Protocol for udp connection. + + Arguments: + queue: The asyncioQueue to put messages into. + + """ + + def __init__(self, queue: asyncio.Queue[bytes]) -> None: + super().__init__() + self.queue = queue + + def datagram_received(self, data: bytes, addr: tuple[str | Any, int]) -> None: + """Handle received udp message.""" + self.queue.put_nowait(data) + + def connection_made(self, transport: asyncio.DatagramTransport) -> None: # type: ignore[override] + """Log successful connection.""" + logger.info("UdpSource: Connection made") + + +class UdpReceiver(Receiver[T]): + """An asynchronous UDP subscriber based on asyncio.protocols.DatagramProtocol.""" + + def __init__(self, config: UdpConf, unpacker: Unpacker[T] = json_unpacker) -> None: # type: ignore[assignment] + self.config = config + self.EOF = self.config.delimiter.encode(self.config.encoding) + self.unpacker = unpacker + self._queue: asyncio.Queue[bytes] = asyncio.Queue(maxsize=self.config.max_queue_size) + self._task: asyncio.Task[None] | None = None + self._is_connected = False + self._transport: asyncio.DatagramTransport | None = None + self._protocol: asyncio.DatagramProtocol | None = None + + async def start(self) -> None: + """Start udp connection.""" + loop = asyncio.get_event_loop() + self._transport, self._protocol = await loop.create_datagram_endpoint( + lambda: UdpProtocol(self._queue), + local_addr=(self.config.host, self.config.port), + ) + self._is_connected = True + logger.info("Udp connection established.") + + async def stop(self) -> None: + """Stop the udp connection.""" + if self._transport is not None: + self._transport.close() + self._transport = None + self._is_connected = False + + async def receive(self) -> tuple[T, dict[str, Any]]: + """Get the next message from the udp connection. + + Returns: + tuple[T, dict[str, Any]] + - The data as returned by the unpacker. + - A dictionary containing extra information. + + Raises: + UnpackError: If the received message could not be unpacked. + + """ + if not self._is_connected: + await self.start() + + while True: + data = None + data = await self._queue.get() + payload, extra = self.unpacker(data) + return (payload, extra) + + def __repr__(self) -> str: + """Return string representation of UdpSource.""" + return f"{self.__class__.__name__}(host={self.config.host}, port={self.config.port})" diff --git a/src/heisskleber/udp/sender.py b/src/heisskleber/udp/sender.py new file mode 100644 index 0000000..8693db4 --- /dev/null +++ b/src/heisskleber/udp/sender.py @@ -0,0 +1,88 @@ +import asyncio +import logging +from typing import Any, TypeVar + +from heisskleber.core import Packer, Sender, json_packer +from heisskleber.udp.config import UdpConf + +logger = logging.getLogger("heisskleber.udp") + +T = TypeVar("T") + + +class UdpProtocol(asyncio.DatagramProtocol): + """UDP protocol handler that tracks connection state. + + Arguments: + is_connected: Flag tracking if protocol is connected + + """ + + def __init__(self, is_connected: bool) -> None: + super().__init__() + self.is_connected = is_connected + + def connection_lost(self, exc: Exception | None) -> None: + """Update state and log a lost connection.""" + logger.info("UDP Connection lost") + self.is_connected = False + + +class UdpSender(Sender[T]): + """UDP sink for sending data via UDP protocol. + + Arguments: + config: UDP configuration parameters + packer: Function to serialize data, defaults to JSON packing + + """ + + def __init__(self, config: UdpConf, packer: Packer[T] = json_packer) -> None: # type: ignore[assignment] + self.config = config + self.pack = packer + self.is_connected = False + self._transport: asyncio.DatagramTransport | None = None + self._protocol: UdpProtocol | None = None + + async def start(self) -> None: + """Connect the UdpSink.""" + await self._ensure_connection() + + async def stop(self) -> None: + """Disconnect the UdpSink connection.""" + if self._transport is not None: + self._transport.close() + self.is_connected = False + self._transport = None + self._protocol = None + + async def _ensure_connection(self) -> None: + """Create UDP endpoint if not connected. + + Creates datagram endpoint using protocol handler if no connection exists. + Updates connected state on successful connection. + + """ + if not self.is_connected or self._transport is None: + loop = asyncio.get_running_loop() + self._transport, _ = await loop.create_datagram_endpoint( + lambda: UdpProtocol(self.is_connected), + remote_addr=(self.config.host, self.config.port), + ) + self.is_connected = True + + async def send(self, data: T, **kwargs: dict[str, Any]) -> None: + """Send data over UDP connection. + + Arguments: + data: Data to send + **kwargs: Additional arguments passed to send + + """ + await self._ensure_connection() # we know that self._transport is intialized + payload = self.pack(data) + self._transport.sendto(payload) # type: ignore [union-attr] + + def __repr__(self) -> str: + """Return string representation of UdpSink.""" + return f"{self.__class__.__name__}(host={self.config.host}, port={self.config.port})" diff --git a/src/heisskleber/zmq/__init__.py b/src/heisskleber/zmq/__init__.py new file mode 100644 index 0000000..b1e9749 --- /dev/null +++ b/src/heisskleber/zmq/__init__.py @@ -0,0 +1,5 @@ +from .config import ZmqConf +from .receiver import ZmqReceiver +from .sender import ZmqSender + +__all__ = ["ZmqConf", "ZmqSender", "ZmqReceiver"] diff --git a/heisskleber/zmq/config.py b/src/heisskleber/zmq/config.py similarity index 69% rename from heisskleber/zmq/config.py rename to src/heisskleber/zmq/config.py index a93a164..03e472d 100644 --- a/heisskleber/zmq/config.py +++ b/src/heisskleber/zmq/config.py @@ -1,10 +1,12 @@ from dataclasses import dataclass -from heisskleber.config import BaseConf +from heisskleber.core import BaseConf @dataclass class ZmqConf(BaseConf): + """ZMQ Configuration file.""" + protocol: str = "tcp" host: str = "127.0.0.1" publisher_port: int = 5555 @@ -13,8 +15,10 @@ class ZmqConf(BaseConf): @property def publisher_address(self) -> str: + """Return the full url to connect to the publisher port.""" return f"{self.protocol}://{self.host}:{self.publisher_port}" @property def subscriber_address(self) -> str: + """Return the full url to connect to the subscriber port.""" return f"{self.protocol}://{self.host}:{self.subscriber_port}" diff --git a/src/heisskleber/zmq/receiver.py b/src/heisskleber/zmq/receiver.py new file mode 100644 index 0000000..9db4ab1 --- /dev/null +++ b/src/heisskleber/zmq/receiver.py @@ -0,0 +1,85 @@ +import logging +from typing import Any, TypeVar + +import zmq +import zmq.asyncio + +from heisskleber.core import Receiver, Unpacker, json_unpacker +from heisskleber.zmq.config import ZmqConf + +logger = logging.getLogger("heisskleber.zmq") + + +T = TypeVar("T") + + +class ZmqReceiver(Receiver[T]): + """Async source that subscribes to one or many topics from a zmq broker and receives messages via the receive() function. + + Attributes: + config: The ZmqConf configuration object for the connection. + unpacker : The unpacker function to use for deserializing the data. + + + """ + + def __init__(self, config: ZmqConf, topic: str | list[str], unpacker: Unpacker[T] = json_unpacker) -> None: # type: ignore [assignment] + self.config = config + self.topic = topic + self.context = zmq.asyncio.Context.instance() + self.socket: zmq.asyncio.Socket = self.context.socket(zmq.SUB) + self.unpack = unpacker + self.is_connected = False + + async def receive(self) -> tuple[T, dict[str, Any]]: + """Read a message from the zmq bus and return it. + + Returns: + tuple(topic: str, message: dict): the message received + + Raises: + UnpackError: If payload could not be unpacked with provided unpacker. + + """ + if not self.is_connected: + await self.start() + (topic, payload) = await self.socket.recv_multipart() + data, extra = self.unpack(payload) + extra["topic"] = topic.decode() + return data, extra + + async def start(self) -> None: + """Connect to the zmq socket.""" + try: + self.socket.connect(self.config.subscriber_address) + except Exception: + logger.exception("Failed to bind to zeromq socket") + else: + self.is_connected = True + self.subscribe(self.topic) + + async def stop(self) -> None: + """Close the zmq socket.""" + self.socket.close() + self.is_connected = False + + def subscribe(self, topic: str | list[str] | tuple[str]) -> None: + """Subscribe to the given topic(s) on the zmq socket. + + Arguments: + --------- + topic: The topic or list of topics to subscribe to. + + """ + if isinstance(topic, (list, tuple)): + for t in topic: + self._subscribe_single_topic(t) + else: + self._subscribe_single_topic(topic) + + def _subscribe_single_topic(self, topic: str) -> None: + self.socket.setsockopt(zmq.SUBSCRIBE, topic.encode()) + + def __repr__(self) -> str: + """Return string representation of ZmqSource.""" + return f"{self.__class__.__name__}(host={self.config.subscriber_address}, port={self.config.subscriber_port})" diff --git a/src/heisskleber/zmq/sender.py b/src/heisskleber/zmq/sender.py new file mode 100644 index 0000000..955c393 --- /dev/null +++ b/src/heisskleber/zmq/sender.py @@ -0,0 +1,54 @@ +import logging +from typing import Any, TypeVar + +import zmq +import zmq.asyncio + +from heisskleber.core import Packer, Sender, json_packer + +from .config import ZmqConf + +logger = logging.getLogger("heisskleber.zmq") + +T = TypeVar("T") + + +class ZmqSender(Sender[T]): + """Async publisher that sends messages to a ZMQ PUB socket. + + Attributes: + config: The ZmqConf configuration object for the connection. + packer : The packer strategy to use for serializing the data. + Defaults to json packer with utf-8 encoding. + + """ + + def __init__(self, config: ZmqConf, packer: Packer[T] = json_packer) -> None: # type: ignore[assignment] + self.config = config + self.context = zmq.asyncio.Context.instance() + self.socket: zmq.asyncio.Socket = self.context.socket(zmq.PUB) + self.packer = packer + self.is_connected = False + + async def send(self, data: T, topic: str = "zmq", **kwargs: Any) -> None: + """Take the data as a dict, serialize it with the given packer and send it to the zmq socket.""" + if not self.is_connected: + await self.start() + payload = self.packer(data) + logger.debug("sending payload %(payload)b to topic %(topic)s", {"payload": payload, "topic": topic}) + await self.socket.send_multipart([topic.encode(), payload]) + + async def start(self) -> None: + """Connect to the zmq socket.""" + logger.info("Connecting to %(addr)s", {"addr": self.config.publisher_address}) + self.socket.connect(self.config.publisher_address) + self.is_connected = True + + async def stop(self) -> None: + """Close the zmq socket.""" + self.socket.close() + self.is_connected = False + + def __repr__(self) -> str: + """Return string representation of ZmqSink.""" + return f"{self.__class__.__name__}(host={self.config.publisher_address}, port={self.config.publisher_port})" diff --git a/tests/config.yaml b/tests/config.yaml deleted file mode 100644 index 24beab0..0000000 --- a/tests/config.yaml +++ /dev/null @@ -1,2 +0,0 @@ -verbose: True -print_stdout: False diff --git a/heisskleber/console/__init__.py b/tests/console/__init__.py similarity index 100% rename from heisskleber/console/__init__.py rename to tests/console/__init__.py diff --git a/tests/console/test_console_sink.py b/tests/console/test_console_sink.py new file mode 100644 index 0000000..5598bfc --- /dev/null +++ b/tests/console/test_console_sink.py @@ -0,0 +1,13 @@ +import pytest + +from heisskleber.console import ConsoleSender + + +@pytest.mark.asyncio +async def test_console_sink(capsys) -> None: + sink = ConsoleSender() + await sink.send({"key": 3}, "test") + + captured = capsys.readouterr() + + assert captured.out == 'test:\t{"key": 3}\n' diff --git a/heisskleber/core/__init__.py b/tests/core/__init__.py similarity index 100% rename from heisskleber/core/__init__.py rename to tests/core/__init__.py diff --git a/tests/core/test_conf.yaml b/tests/core/test_conf.yaml new file mode 100644 index 0000000..707c5e9 --- /dev/null +++ b/tests/core/test_conf.yaml @@ -0,0 +1,3 @@ +name: Frodo +age: 30 +speed: 0.5 diff --git a/tests/core/test_config.py b/tests/core/test_config.py new file mode 100644 index 0000000..e5041a6 --- /dev/null +++ b/tests/core/test_config.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass + +import pytest + +from heisskleber.core import BaseConf + + +@dataclass +class SampleConfig(BaseConf): + name: str + age: int = 18 + speed: float = 1.05 + + +def test_config_constructor() -> None: + test_conf = SampleConfig(name="Gandalf", age=200, speed=10.0) + assert test_conf.name == "Gandalf" + assert test_conf.age == 200 + assert test_conf.speed == 10.0 + + +def test_config_from_dict() -> None: + test_dict = {"name": "Alice", "age": 30, "job": "Electrician", "speed": 1.0} + + expected = SampleConfig(name="Alice", age=30, speed=1.0) + + configuration = SampleConfig.from_dict(test_dict) + assert configuration == expected + + +def test_config_from_dict_with_default() -> None: + test_dict = {"name": "Alice"} + + expected = SampleConfig(name="Alice", age=18) + + configuration = SampleConfig.from_dict(test_dict) + assert configuration == expected + + +def test_pytest_raises_type_error() -> None: + with pytest.raises(TypeError): + SampleConfig(name=1.0) # type: ignore[arg-type] + + +def test_pytest_raises_type_error_from_dict() -> None: + with pytest.raises(TypeError): + SampleConfig.from_dict({"name": 1.0, "age": "monkey"}) + + +def test_conf_from_file() -> None: + test_conf = SampleConfig.from_file("./tests/core/test_conf.yaml") + + assert test_conf.name == "Frodo" + assert test_conf.age == 30 + assert test_conf.speed == 0.5 diff --git a/tests/core/test_packer.py b/tests/core/test_packer.py new file mode 100644 index 0000000..c58685f --- /dev/null +++ b/tests/core/test_packer.py @@ -0,0 +1,65 @@ +import json +from dataclasses import dataclass +from typing import Any + +import pytest + +from heisskleber.core import Packer, json_packer +from heisskleber.core.packer import PackerError + + +@pytest.fixture +def packer() -> Packer[dict[str, Any]]: + return json_packer + + +def test_simple_dict(packer: Packer[dict[str, Any]]) -> None: + """Test packing a simple dictionary""" + test_data = {"key": "value"} + result = packer(test_data) + assert result == b'{"key": "value"}' + # Verify it can be decoded back + assert json.loads(result.decode()) == test_data + + +def test_nested_dict(packer: Packer[dict[str, Any]]) -> None: + """Test packing a nested dictionary""" + test_data = {"string": "value", "number": 42, "nested": {"bool": True, "list": [1, 2, 3]}} + result = packer(test_data) + # Verify it can be decoded back to the same structure + assert json.loads(result.decode()) == test_data + + +def test_empty_dict(packer: Packer[dict[str, Any]]) -> None: + """Test packing an empty dictionary""" + test_data = {} + result = packer(test_data) + assert result == b"{}" + + +def test_special_characters(packer: Packer[dict[str, Any]]) -> None: + """Test packing data with special characters""" + test_data = {"special": "Hello\nWorld\t!", "unicode": "🌍🌎🌏"} + result = packer(test_data) + # Verify it can be decoded back + assert json.loads(result.decode()) == test_data + + +def test_non_serializable_values(packer: Packer[dict[str, Any]]) -> None: + """Test that non-JSON-serializable values raise TypeError""" + + @dataclass + class NonSerializable: + x: int + + test_data = {"key": NonSerializable(42)} + with pytest.raises(PackerError): + packer(test_data) + + +def test_large_dict(packer: Packer[dict[str, Any]]) -> None: + """Test packing a large dictionary""" + test_data = {str(i): f"value_{i}" for i in range(1000)} + result = packer(test_data) + # Verify it can be decoded back + assert json.loads(result.decode()) == test_data diff --git a/tests/core/test_unpacker.py b/tests/core/test_unpacker.py new file mode 100644 index 0000000..af56ace --- /dev/null +++ b/tests/core/test_unpacker.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass +from typing import Any + +from heisskleber.core import Unpacker, json_unpacker + + +@dataclass +class HotGlue: + name: str + weight: float + + +class DataclassUnpacker(Unpacker[HotGlue]): + """Take a csv of 'name,weight' and construct a HotGlue dataclass. + + Raises: + TypeError: The csv was not formatted properly or a dataclass could not be constructed. + + """ + + def __call__(self, payload: bytes) -> tuple[HotGlue, dict[str, Any]]: + extra = {"type": "custom"} + try: + name, weight = payload.decode().split(",") + return HotGlue(name, float(weight)), extra + except UnicodeDecodeError as e: + raise TypeError from e + + +def test_custom_unpacker() -> None: + unpacker = DataclassUnpacker() + + data, extra = unpacker(b"hotglue,10.0") + assert isinstance(data, HotGlue) + assert data == HotGlue("hotglue", 10.0) + + assert extra == {"type": "custom"} + + +def test_simple_bytestring() -> None: + """Test packing a simple dictionary""" + + test_data = b'{"key": "value"}' + data, extra = json_unpacker(test_data) + assert data == {"key": "value"} + assert extra == {} + + +def test_nested_dict() -> None: + """Test packing a nested dictionary""" + test_data = b'{"string": "value", "number": 42, "nested": {"bool": true, "list": [1, 2, 3]}}' + data, extra = json_unpacker(test_data) + + assert data == {"string": "value", "number": 42, "nested": {"bool": True, "list": [1, 2, 3]}} + assert extra == {} diff --git a/tests/integration/async_streamer.py b/tests/integration/async_streamer.py deleted file mode 100644 index cde1761..0000000 --- a/tests/integration/async_streamer.py +++ /dev/null @@ -1,43 +0,0 @@ -import asyncio - -import numpy as np - -from heisskleber.mqtt import AsyncMqttSubscriber, MqttConf -from heisskleber.stream.resampler import Resampler, ResamplerConf - - -async def main(): - topic1 = "topic1" - topic2 = "topic2" - - config = MqttConf(host="localhost", port=1883, user="", password="") # not a real password - sub1 = AsyncMqttSubscriber(config, topic1) - sub2 = AsyncMqttSubscriber(config, topic2) - - resampler_config = ResamplerConf(resample_rate=250) - - resampler1 = Resampler(resampler_config, sub1) - resampler2 = Resampler(resampler_config, sub2) - - while True: - m1, m2 = await asyncio.gather(resampler1.receive(), resampler2.receive()) - - print(f"epoch: {m1['epoch']}") - print(f"diff: {diff(m1, m2)}") - - -def diff(dict1, dict2): - return dict( - zip( - dict1.keys(), - np.array(list(dict1.values())) - np.array(list(dict2.values())), - ) - ) - - -# Run the event loop -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - print("Keyboard Interrupt") diff --git a/tests/integration/integration_joint.py b/tests/integration/integration_joint.py deleted file mode 100644 index 84caa9d..0000000 --- a/tests/integration/integration_joint.py +++ /dev/null @@ -1,27 +0,0 @@ -import asyncio - -from heisskleber.mqtt import AsyncMqttSubscriber, MqttConf -from heisskleber.stream import Joint, Resampler, ResamplerConf - - -async def main(): - topics = ["topic0", "topic1", "topic2", "topic3"] - - config = MqttConf(host="localhost", port=1883, user="", password="") # not a real password - subs = [AsyncMqttSubscriber(config, topic=topic) for topic in topics] - - resampler_config = ResamplerConf(resample_rate=1000) - - joint = Joint(resampler_config, [Resampler(resampler_config, sub) for sub in subs]) - - while True: - data = await joint.receive() - print(data) - - -# Run the event loop -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - print("Keyboard Interrupt") diff --git a/tests/integration/mqtt_async.py b/tests/integration/mqtt_async.py deleted file mode 100644 index c847a43..0000000 --- a/tests/integration/mqtt_async.py +++ /dev/null @@ -1,18 +0,0 @@ -import asyncio - -from heisskleber.mqtt import AsyncMqttSubscriber, MqttConf - - -async def main(): - conf = MqttConf(host="localhost", port=1883, user="", password="") - sub = AsyncMqttSubscriber(conf, topic="#") - # async for topic, message in sub: - # print(message) - # _ = asyncio.create_task(sub.run()) - while True: - topic, message = await sub.receive() - print(message) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/tests/integration/mqtt_pub.py b/tests/integration/mqtt_pub.py deleted file mode 100644 index 678e8e2..0000000 --- a/tests/integration/mqtt_pub.py +++ /dev/null @@ -1,35 +0,0 @@ -import asyncio -import time - -from termcolor import colored - -from heisskleber.mqtt import AsyncMqttPublisher, MqttConf - -colortable = ["red", "green", "yellow", "blue", "magenta", "cyan"] - - -async def send_every_n_miliseconds(frequency, value, pub, topic): - start = time.time() - while True: - epoch = time.time() - start - payload = {"epoch": epoch, f"value{value}": value} - print_message = f"Pub #{int(value)} sending {payload}" - print(colored(print_message, colortable[int(value)])) - await pub.send(payload, topic) - await asyncio.sleep(frequency) - - -async def main2(): - config = MqttConf(host="localhost", port=1883, user="", password="") - - pubs = [AsyncMqttPublisher(config) for i in range(5)] - tasks = [] - for i, pub in enumerate(pubs): - tasks.append(asyncio.create_task(send_every_n_miliseconds(1 + i * 0.1, i, pub, f"topic{i}"))) - - await asyncio.gather(*tasks) - - -if __name__ == "__main__": - # main() - asyncio.run(main2()) diff --git a/tests/integration/mqtt_stream.py b/tests/integration/mqtt_stream.py deleted file mode 100644 index 5ac2308..0000000 --- a/tests/integration/mqtt_stream.py +++ /dev/null @@ -1,19 +0,0 @@ -import asyncio - -from heisskleber.mqtt import AsyncMqttSubscriber, MqttConf -from heisskleber.stream import Resampler, ResamplerConf - - -async def main(): - conf = MqttConf(host="localhost", port=1883, user="", password="") - sub = AsyncMqttSubscriber(conf, topic="#") - - resampler = Resampler(ResamplerConf(), sub) - - while True: - data = await resampler.receive() - print(data) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/tests/integration/mqtt_sub.py b/tests/integration/mqtt_sub.py deleted file mode 100644 index 55bce37..0000000 --- a/tests/integration/mqtt_sub.py +++ /dev/null @@ -1,17 +0,0 @@ -import asyncio - -from heisskleber.mqtt import AsyncMqttSubscriber, MqttConf - - -async def main(): - config = MqttConf(host="localhost", port=1883, user="", password="") - - sub = AsyncMqttSubscriber(config, topic="#") - - while True: - topic, data = await sub.receive() - print(f"topic: {topic}, data: {data}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/tests/integration/sync_streamer.py b/tests/integration/sync_streamer.py deleted file mode 100644 index eb39f24..0000000 --- a/tests/integration/sync_streamer.py +++ /dev/null @@ -1,39 +0,0 @@ -import threading -from collections import namedtuple - -from heisskleber.mqtt import MqttConf, MqttSubscriber -from heisskleber.stream.sync_resampler import Resampler - - -def main(): - # topic1 = "/msb-fwd-body/imu" - # topic2 = "/msb-102-a/imu" - # topic2 = "/msb-102-a/rpy" - topic1 = "topic1" - # topic2 = "topic2" - - config = MqttConf( - host="localhost", port=1883, user="", password="" - ) # , not a real password port=1883, user="", password="") - sub1 = MqttSubscriber(config, topic1) - # sub2 = MqttSubscriber(config, topic2) - - resampler_config = namedtuple("config", "resample_rate")(1000) - - resampler1 = Resampler(resampler_config, sub1) - - t1 = threading.Thread(target=resampler1.run) - t1.start() - - # async for resampled_dict in resampler2.resample(): - # print(resampled_dict) - - try: - for m1 in zip(resampler1.resample()): - print(m1) - finally: - t1.join() - - -if __name__ == "__main__": - main() diff --git a/tests/integration/udp/udp_integration.py b/tests/integration/udp/udp_integration.py deleted file mode 100644 index 25c3f18..0000000 --- a/tests/integration/udp/udp_integration.py +++ /dev/null @@ -1,36 +0,0 @@ -import asyncio -import sys -from time import time - -from heisskleber.udp import AsyncUdpSink, AsyncUdpSource, UdpConf - - -async def publish_forever(): - conf = UdpConf(host="127.0.0.1", port=12345) - pub = AsyncUdpSink(conf) - - while True: - await asyncio.sleep(1) - await pub.send({"epoch": time(), "data": "Hello, world!"}, "udp") - - -async def listen_forever() -> None: - conf = UdpConf(host="127.0.0.1", port=12345) - sub = AsyncUdpSource(conf) - print(f"Started subscriber: {sub}") - - while True: - _, payload = await sub.receive() - print(f"Received payload: {payload}") - - -async def main(): - await asyncio.gather(publish_forever(), listen_forever()) - - -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - print("Exiting...") - sys.exit(0) diff --git a/tests/integration/udp/udp_listener.py b/tests/integration/udp/udp_listener.py deleted file mode 100644 index b33b545..0000000 --- a/tests/integration/udp/udp_listener.py +++ /dev/null @@ -1,14 +0,0 @@ -from heisskleber.udp import UdpConf, UdpSubscriber - - -def main() -> None: - conf = UdpConf(host="localhost", port=12345) - sub = UdpSubscriber(conf) - - while True: - _, payload = sub.receive() - print(f"Received payload: {payload}") - - -if __name__ == "__main__": - main() diff --git a/tests/integration/udp/udp_publisher.py b/tests/integration/udp/udp_publisher.py deleted file mode 100644 index ee804fa..0000000 --- a/tests/integration/udp/udp_publisher.py +++ /dev/null @@ -1,16 +0,0 @@ -from time import sleep - -from heisskleber.udp import UdpConf, UdpPublisher - - -def main() -> None: - conf = UdpConf(host="localhost", port=12345) - pub = UdpPublisher(conf) - - while True: - pub.send({"data": "hello"}, topic="test") - sleep(1.0) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/zmq_pub.py b/tests/integration/zmq_pub.py deleted file mode 100644 index 7de1429..0000000 --- a/tests/integration/zmq_pub.py +++ /dev/null @@ -1,17 +0,0 @@ -import time - -from heisskleber import get_sink - - -def main(): - sink = get_sink("zmq") - - i = 0 - while True: - sink.send({"test pub": i}, "test") - time.sleep(1) - i += 1 - - -if __name__ == "__main__": - main() diff --git a/heisskleber/influxdb/__init__.py b/tests/mqtt/__init__.py similarity index 100% rename from heisskleber/influxdb/__init__.py rename to tests/mqtt/__init__.py diff --git a/tests/mqtt/test_mqtt_conf.py b/tests/mqtt/test_mqtt_conf.py new file mode 100644 index 0000000..42568e8 --- /dev/null +++ b/tests/mqtt/test_mqtt_conf.py @@ -0,0 +1,21 @@ +from heisskleber.mqtt.config import MqttConf, Will + + +def test_create_config_with_will() -> None: + config_dict = { + "host": "example.com", + "port": 1883, + "ssl": False, + "timeout": 60, + "keep_alive": 60, + "will": {"topic": "will_topic", "payload": "I didn't make it", "retain": False, "properties": None}, + } + + conf = MqttConf.from_dict(config_dict) + + assert conf.will == Will(topic="will_topic", payload="I didn't make it", retain=False, properties=None) + assert conf.host == "example.com" + assert conf.port == 1883 + assert conf.ssl is False + assert conf.timeout == 60 + assert conf.keep_alive == 60 diff --git a/tests/mqtt/test_mqtt_sink.py b/tests/mqtt/test_mqtt_sink.py new file mode 100644 index 0000000..64e7656 --- /dev/null +++ b/tests/mqtt/test_mqtt_sink.py @@ -0,0 +1,30 @@ +import asyncio +from unittest.mock import AsyncMock, Mock, patch + +import pytest + +from heisskleber.mqtt import MqttConf, MqttSender + + +@pytest.mark.asyncio +async def test_send_work_successful_publish() -> None: + """Test successful message publishing""" + mqtt_config = MqttConf() + mock_packer = Mock(return_value=b'{"test": "data"}') + sink = MqttSender(config=mqtt_config, packer=mock_packer) + + # Mock MQTT client + mock_client = AsyncMock() + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock() + + with patch("aiomqtt.Client", return_value=mock_client): + test_data = {"test": "data"} + test_topic = "test/topic" + await sink.send(test_data, test_topic) + + await asyncio.sleep(0.1) + + mock_client.publish.assert_awaited_once_with(topic=test_topic, payload=mock_packer.return_value) + + await sink.stop() diff --git a/tests/mqtt/test_mqtt_source.py b/tests/mqtt/test_mqtt_source.py new file mode 100644 index 0000000..c08425b --- /dev/null +++ b/tests/mqtt/test_mqtt_source.py @@ -0,0 +1,33 @@ +from unittest.mock import AsyncMock, patch + +import aiomqtt +import pytest + +from heisskleber.core.unpacker import JSONUnpacker +from heisskleber.mqtt import MqttConf, MqttReceiver + + +@pytest.mark.asyncio +async def test_mqtt_source_receive_message() -> None: + """Test successful message reception and unpacking""" + + # Mock MQTT client + mock_client = AsyncMock() + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock() + + with patch("aiomqtt.Client", return_value=mock_client): + mqtt_source = MqttReceiver(config=MqttConf(), topic="test", unpacker=JSONUnpacker()) + + test_payload = b'{"test":"data"}' + test_topic = "test/topic" + message = aiomqtt.Message(topic=test_topic, payload=test_payload, qos=0, retain=False, mid=1, properties=None) + await mqtt_source._message_queue.put(message) + + data, extra = await mqtt_source.receive() + + assert data == {"test": "data"} + assert "topic" in extra + assert extra["topic"] == test_topic + + await mqtt_source.stop() diff --git a/tests/serial/Dockerfile b/tests/serial/Dockerfile new file mode 100644 index 0000000..0f3ef78 --- /dev/null +++ b/tests/serial/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11 + +# Install socat +RUN apt-get update && apt-get install -y socat + +# Set up your application directory +WORKDIR /app + +# Copy your code into the container +COPY . /app + +# Install dependencies +RUN pip install -r requirements.txt + +# Run socat to create virtual ports and start pytest +CMD socat -d -d pty,raw,echo=0,link=/app/writer pty,raw,echo=0,link=/app/reader & sleep 1 && pytest tests/serial/test_serial.py diff --git a/tests/integration/__init__.py b/tests/serial/__init__.py similarity index 100% rename from tests/integration/__init__.py rename to tests/serial/__init__.py diff --git a/tests/serial/functional_test_serial.py b/tests/serial/functional_test_serial.py new file mode 100644 index 0000000..e2866c8 --- /dev/null +++ b/tests/serial/functional_test_serial.py @@ -0,0 +1,39 @@ +import asyncio +import json +from typing import Any + +import pytest +import serial + +from heisskleber.serial import SerialConf, SerialReceiver, SerialSender + + +def serial_unpacker(payload: bytes) -> tuple[dict[str, Any], dict[str, Any]]: + return (json.loads(payload), {}) + + +def serial_packer(data: dict[str, Any]) -> bytes: + return (json.dumps(data) + "\n").encode() + + +@pytest.mark.asyncio +async def test_serial_with_ser() -> None: + writer_port, reader_port = "./writer", "./reader" + await asyncio.sleep(1) + conf = SerialConf( + port=reader_port, + baudrate=9600, + ) + source = SerialReceiver(conf, unpack=serial_unpacker) + + await asyncio.sleep(0.1) + + writer = serial.Serial(port=writer_port, baudrate=9600) + writer.write(b'{"data": "test"}\n') + writer.flush() + + sink = SerialSender(SerialConf(port=writer_port, baudrate=9600), pack=serial_packer) + await sink.send({"data": "test"}) + + data, extra = await source.receive() + assert data == {"data": "test"} diff --git a/tests/stream/test_async_mqtt.py b/tests/stream/test_async_mqtt.py deleted file mode 100644 index 27c4611..0000000 --- a/tests/stream/test_async_mqtt.py +++ /dev/null @@ -1,80 +0,0 @@ -import json -from unittest.mock import AsyncMock - -import pytest - -from heisskleber.mqtt import AsyncMqttSubscriber -from heisskleber.mqtt.config import MqttConf - - -class MockAsyncClient: - def __init__(self): - self.messages = AsyncMock() - self.messages.return_value = [{"epoch": i, "data": 1} for i in range(10)] - - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_value, traceback): - pass - - async def subscribe(self, *args): - pass - - -@pytest.fixture -def mock_client(): - return MockAsyncClient() - - -@pytest.fixture -def mock_queue(): - return AsyncMock() - - -@pytest.mark.asyncio -async def test_subscribe_topics_single(mock_queue): - mock_client = AsyncMock() - config = MqttConf() - topics = "single_topic" - sub = AsyncMqttSubscriber(config, topics) - sub.client = mock_client - sub.message_queue = mock_queue - - await sub._subscribe_topics() - - mock_client.subscribe.assert_called_once_with(topics, config.qos) - - -@pytest.mark.asyncio -async def test_subscribe_topics_multiple(mock_queue): - mock_client = AsyncMock() - config = MqttConf() - topics = ["topic1", "topic2"] - sub = AsyncMqttSubscriber(config, topics) - sub.client = mock_client - sub.message_queue = mock_queue - - await sub._subscribe_topics() - - mock_client.subscribe.assert_called_once_with([(t, config.qos) for t in topics]) - - -@pytest.mark.asyncio -async def test_receive(mock_client, mock_queue): - config = MqttConf() - sub = AsyncMqttSubscriber(config, "some_topic") - sub.client = mock_client - sub.message_queue = mock_queue - - mock_message = AsyncMock() - mock_message.topic = "some_topic" - mock_message.payload = json.dumps({"some": "payload"}).encode("utf-8") - - mock_queue.get.return_value = mock_message - - topic, payload = await sub.receive() - - assert isinstance(topic, str) - assert topic == mock_message.topic - assert payload == {"some": "payload"} diff --git a/heisskleber/config.py b/tests/tcp/__init__.py similarity index 100% rename from heisskleber/config.py rename to tests/tcp/__init__.py diff --git a/tests/tcp/test_tcp_source.py b/tests/tcp/test_tcp_source.py new file mode 100644 index 0000000..b60cf7e --- /dev/null +++ b/tests/tcp/test_tcp_source.py @@ -0,0 +1,190 @@ +import asyncio +import contextlib +import logging +from collections.abc import AsyncGenerator +from typing import Any + +import pytest +import pytest_asyncio + +from heisskleber.tcp import TcpConf, TcpReceiver + + +def bytes_csv_unpacker(payload: bytes) -> tuple[dict[str, Any], dict[str, Any]]: + """Unpack string containing comma separated values to dictionary.""" + vals = payload.decode().rstrip().split(",") + keys = [f"key{i}" for i in range(len(vals))] + return (dict(zip(keys, vals)), {"topic": "tcp"}) + + +port = 23456 +tcp_logger_name = "heisskleber.tcp" + + +class TcpTestSender: + server: asyncio.Server + + def __init__(self): + self.on_connected = self._send_ok + + async def start(self, port): + self.server = await asyncio.start_server(self.handle_connection, port=port) + + async def stop(self): + self.server.close() + await self.server.wait_closed() + + def handle_connection(self, _reader, writer): + self.on_connected(writer) + + def _send_ok(self, writer): + writer.write(b"OK\n") + + +@pytest_asyncio.fixture +# @pytest.mark.asyncio(loop_scope="session") +async def sender() -> AsyncGenerator[TcpTestSender, None]: + sender = TcpTestSender() + yield sender + await sender.stop() + + +@pytest.fixture +def mock_conf(): + return TcpConf(host="127.0.0.1", port=port, restart_behavior=TcpConf.RestartBehavior.NEVER) + + +def test_00_bytes_csv_unpacker() -> None: + unpacker = bytes_csv_unpacker + data, extra = unpacker(b"OK") + assert data == {"key0": "OK"} + assert extra == {"topic": "tcp"} + + +@pytest.mark.asyncio +async def test_01_connect_refused(mock_conf, caplog) -> None: + logger = logging.getLogger(tcp_logger_name) + logger.setLevel(logging.WARNING) + + source = TcpReceiver(mock_conf) + with contextlib.suppress(ConnectionRefusedError): + await source.start() + + assert len(caplog.record_tuples) == 1 + logger_name, level, message = caplog.record_tuples[0] + assert logger_name == "heisskleber.tcp" + assert level == 40 + assert message == f"TcpReceiver(host=127.0.0.1, port={port}): ConnectionRefusedError" + await source.stop() + + +@pytest.mark.asyncio +async def test_02_connect_timedout(mock_conf, caplog) -> None: + logger = logging.getLogger("heisskleber.tcp") + logger.setLevel(logging.WARNING) + + mock_conf.timeout = 1 + source = TcpReceiver(mock_conf) + # Linux "ConnectionRefusedError", Windows says "TimeoutError" + with contextlib.suppress(TimeoutError, ConnectionRefusedError): + await source.start() + assert len(caplog.record_tuples) == 1 + logger_name, level, message = caplog.record_tuples[0] + assert logger_name == tcp_logger_name + assert level == 40 + assert message in ( + f"TcpReceiver(host=127.0.0.1, port={port}): ConnectionRefusedError", + f"TcpReceiver(host=127.0.0.1, port={port}): TimeoutError", + ) + await source.stop() + + +@pytest.mark.asyncio +async def test_03_connect_retry(mock_conf, caplog, sender) -> None: + logger = logging.getLogger(tcp_logger_name) + logger.setLevel(logging.INFO) + + mock_conf.timeout = 1 + mock_conf.restart_behavior = "always" + source = TcpReceiver(mock_conf) + start_task = asyncio.create_task(source.start()) + + async def delayed_start(): + await asyncio.sleep(1.2) + await sender.start(mock_conf.port) + + await asyncio.create_task(delayed_start()) + await start_task + assert len(caplog.record_tuples) >= 3 + logger_name, level, message = caplog.record_tuples[-1] + assert logger_name == tcp_logger_name + assert level == 20 + assert message == f"TcpReceiver(host=127.0.0.1, port={port}) connected successfully!" + await source.stop() + + +@pytest.mark.asyncio +async def test_04_connects_to_socket(mock_conf, caplog, sender) -> None: + logger = logging.getLogger(tcp_logger_name) + logger.setLevel(logging.INFO) + + await sender.start(mock_conf.port) + + source = TcpReceiver(mock_conf) + await source.start() + assert len(caplog.record_tuples) == 2 + logger_name, level, message = caplog.record_tuples[0] + assert logger_name == tcp_logger_name + assert level == 20 + assert message == f"TcpReceiver(host=127.0.0.1, port={port}) waiting for connection." + logger_name, level, message = caplog.record_tuples[1] + assert logger_name == tcp_logger_name + assert level == 20 + assert message == f"TcpReceiver(host=127.0.0.1, port={port}) connected successfully!" + await source.stop() + + +@pytest.mark.asyncio +async def test_05_connection_to_server_lost(mock_conf, sender) -> None: + def test_steps(): + # First connection: close it + writer = yield + writer.close() + + # Second connection: send data + writer = yield + writer.write(b"OK after second connect\n") + + connection_handler = test_steps() # construct the generator + next(connection_handler) # prime the generator + + def handle_incoming_connection(writer): + connection_handler.send(writer) + + sender.on_connected = handle_incoming_connection + + await sender.start(mock_conf.port) + + source = TcpReceiver(mock_conf, unpacker=bytes_csv_unpacker) + data = await source.receive() + _check_data(data, "OK after second connect") + await source.stop() + + +@pytest.mark.asyncio +async def test_06_data_received(mock_conf, sender) -> None: + await sender.start(mock_conf.port) + + source = TcpReceiver(mock_conf, unpacker=bytes_csv_unpacker) + data = await source.receive() + _check_data(data, "OK") + await source.stop() + + +def _check_data(data, expected_value: str): + assert isinstance(data, tuple) + assert len(data) == 2 + result, extra = data + assert result == {"key0": expected_value} + assert isinstance(result, dict) + assert "key0" in result diff --git a/tests/test_config.py b/tests/test_config.py deleted file mode 100644 index 7ebfb65..0000000 --- a/tests/test_config.py +++ /dev/null @@ -1,127 +0,0 @@ -import unittest.mock as mock -from dataclasses import dataclass -from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest - -from heisskleber.config import BaseConf -from heisskleber.config.config import Config -from heisskleber.config.parse import ( - get_cmdline, - get_config_dir, - get_config_filepath, - load_config, - read_yaml_config_file, - update_config, -) - - -@pytest.fixture -def mock_home(monkeypatch): - home = Path("/mock/home") - monkeypatch.setattr(Path, "home", lambda: home) - return home - - -@pytest.fixture -def mock_config_dir(mock_home): - with ( - patch("heisskleber.config.parse.get_config_dir", return_value=mock_home / ".config" / "heisskleber"), - patch("pathlib.Path.is_dir", return_value=True), - ): - yield - - -def test_get_config_dir(mock_home): - with patch("pathlib.Path.is_dir", return_value=True): - config_dir = get_config_dir() - assert config_dir == mock_home / ".config" / "heisskleber" - - -def test_get_config_dir_not_exists(): - with patch("pathlib.Path.is_dir", return_value=False), pytest.raises(FileNotFoundError): - get_config_dir() - - -def test_get_config_filepath(mock_home, mock_config_dir): - filename = "config.yaml" - expected_path = mock_home / ".config" / "heisskleber" / filename - - with patch("pathlib.Path.is_file", return_value=True): - config_filepath = get_config_filepath(filename) - - assert config_filepath == expected_path - - -def test_update_config(): - @dataclass - class MockConf(BaseConf): - existing_key: str = "old" - - config = MockConf(existing_key="old_value") - config_dict = {"existing_key": "new_value", "non_existing_key": "value"} - - updated_config = update_config(config, config_dict) - - assert updated_config.existing_key == "new_value" - - -def test_load_config_no_cmdline(mock_home): - config = MagicMock(spec=Config) - config_dict = {"key": "value"} - filename = "config" - filepath = mock_home / ".config" / "heisskleber" / (filename + ".yaml") - - with ( - patch("heisskleber.config.parse.get_config_dir", return_value=mock_home / ".config" / "heisskleber"), - patch("heisskleber.config.parse.get_config_filepath", return_value=filepath), - patch("heisskleber.config.parse.read_yaml_config_file", return_value=config_dict), - patch("heisskleber.config.parse.update_config", return_value=config) as mock_update_config, - ): - result = load_config(config, filename, read_commandline=False) - - mock_update_config.assert_called_with(config, config_dict) - assert result == config - - -def test_file_exists(): - with pytest.raises(FileNotFoundError): - get_config_filepath("i_do_not_exist.yaml") - - -def test_read_yaml_config_file(): - config_dict = read_yaml_config_file(Path("tests/config.yaml")) - assert config_dict["verbose"] is True - assert config_dict["print_stdout"] is False - - -def test_get_cmdline_patch_argv(): - with mock.patch("sys.argv", ["test.py", "--verbose"]): - assert get_cmdline()["verbose"] is True - - -def test_get_cmdline(): - assert ( - get_cmdline( - [ - "--verbose", - ] - )["verbose"] - is True - ) - - -def test_load_config_from_file(): - with mock.patch("heisskleber.config.parse.get_config_filepath", return_value=Path("tests/config.yaml")): - conf = load_config(BaseConf(), "config", read_commandline=False) - assert conf.verbose is True - - -def test_load_config_from_yaml(): - with mock.patch("heisskleber.config.parse.get_config_filepath", return_value=Path("tests/config.yaml")), mock.patch( - "sys.argv", ["test.py", "--print-stdout"] - ): - conf = load_config(BaseConf(), "config") - assert conf.verbose is True - assert conf.print_stdout is True diff --git a/tests/test_console_sink.py b/tests/test_console_sink.py deleted file mode 100644 index f1a2e43..0000000 --- a/tests/test_console_sink.py +++ /dev/null @@ -1,59 +0,0 @@ -import pytest - -from heisskleber.console.sink import AsyncConsoleSink, ConsoleSink - - -def test_console_sink(capsys) -> None: - sink = ConsoleSink() - sink.send({"key": 3}, "test") - - captured = capsys.readouterr() - - assert captured.out == "{'key': 3}\n" - - -def test_console_sink_verbose(capsys) -> None: - sink = ConsoleSink(verbose=True) - sink.send({"key": 3}, "test") - - captured = capsys.readouterr() - - assert captured.out == "test:\t{'key': 3}\n" - - -def test_console_sink_pretty(capsys) -> None: - sink = ConsoleSink(pretty=True) - sink.send({"key": 3}, "test") - - captured = capsys.readouterr() - - assert captured.out == '{\n "key": 3\n}\n' - - -def test_console_sink_pretty_verbose(capsys) -> None: - sink = ConsoleSink(pretty=True, verbose=True) - sink.send({"key": 3}, "test") - - captured = capsys.readouterr() - - assert captured.out == 'test:\t{\n "key": 3\n}\n' - - -def test_console_repr() -> None: - sink = ConsoleSink() - assert repr(sink) == "ConsoleSink(pretty=False, verbose=False)" - - -def test_async_console_repr() -> None: - sink = AsyncConsoleSink() - assert repr(sink) == "AsyncConsoleSink(pretty=False, verbose=False)" - - -@pytest.mark.asyncio -async def test_async_console_sink(capsys) -> None: - sink = AsyncConsoleSink() - await sink.send({"key": 3}, "test") - - captured = capsys.readouterr() - - assert captured.out == "{'key': 3}\n" diff --git a/tests/test_factories.py b/tests/test_factories.py deleted file mode 100644 index d2ec3b2..0000000 --- a/tests/test_factories.py +++ /dev/null @@ -1,75 +0,0 @@ -from unittest.mock import patch - -import pytest - -from heisskleber import get_publisher, get_sink, get_source, get_subscriber -from heisskleber.mqtt import MqttConf, MqttPublisher, MqttSubscriber -from heisskleber.serial import SerialConf, SerialPublisher, SerialSubscriber -from heisskleber.zmq import ZmqConf, ZmqPublisher, ZmqSubscriber - - -@pytest.fixture(autouse=True) -def mock_connections(): - with ( - patch("heisskleber.mqtt.mqtt_base.mqtt_client", autospec=True), - patch("heisskleber.zmq.publisher.zmq.Context", autospec=True), - patch("heisskleber.serial.subscriber.serial.Serial"), - ): - yield - - -@pytest.mark.skip -@pytest.mark.parametrize( - "name,pubtype,conftype", - [ - ("mqtt", MqttPublisher, MqttConf), - ("zmq", ZmqPublisher, ZmqConf), - ("serial", SerialPublisher, SerialConf), - ], -) -def test_get_publisher(name, pubtype, conftype): - pub = get_publisher(name) - assert isinstance(pub, pubtype) - assert isinstance(pub.config, conftype) - - -@pytest.mark.skip -@pytest.mark.parametrize( - "name,subtype", - [ - ("mqtt", MqttSubscriber), - ("zmq", ZmqSubscriber), - ("serial", SerialSubscriber), - ], -) -def test_get_subscriber(name, subtype): - sub = get_subscriber(name, "topic") - assert isinstance(sub, subtype) - - -@pytest.mark.skip -@pytest.mark.parametrize( - "name,sinktype", - [ - ("mqtt", MqttPublisher), - ("zmq", ZmqPublisher), - ("serial", SerialPublisher), - ], -) -def test_get_sink(name, sinktype): - pub = get_sink(name) - assert isinstance(pub, sinktype) - - -@pytest.mark.skip -@pytest.mark.parametrize( - "name,sourcetype", - [ - ("mqtt", MqttSubscriber), - ("zmq", ZmqSubscriber), - ("serial", SerialSubscriber), - ], -) -def test_get_source(name, sourcetype): - sub = get_source(name, "topic") - assert isinstance(sub, sourcetype) diff --git a/tests/test_joint.py b/tests/test_joint.py deleted file mode 100644 index 828cb79..0000000 --- a/tests/test_joint.py +++ /dev/null @@ -1,63 +0,0 @@ -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from heisskleber.mqtt import AsyncMqttSubscriber -from heisskleber.stream import Joint, Resampler, ResamplerConf - - -@pytest.fixture -def mock_subscriber(): - return MagicMock() - - -class EndofData(Exception): - pass - - -@pytest.mark.asyncio -async def test_two_streams_are_parallel(): - """ - Test that Joint can synchronize two streams that start at different epoch values. - - The setup includes the mocked subscribers async receive() and run() methods. - """ - sub1 = MagicMock(autospec=AsyncMqttSubscriber) - sub1.receive.side_effect = AsyncMock( - side_effect=[ - ("topic1", {"epoch": 0, "x": 0}), - ("topic1", {"epoch": 1, "x": 1}), - ("topic1", {"epoch": 2, "x": 2}), - ("topic1", {"epoch": 3, "x": 3}), - ("topic1", {"epoch": 4, "x": 4}), - ("topic1", {"epoch": 5, "x": 5}), - ("topic1", {"epoch": 6, "x": 6}), - EndofData(), - ] - ) - sub1.run = AsyncMock() - sub2 = MagicMock(autospec=AsyncMqttSubscriber) - sub2.run = AsyncMock() - sub2.receive.side_effect = AsyncMock( - side_effect=[ - ("topic2", {"epoch": 2, "y": 0}), - ("topic2", {"epoch": 3, "y": 1}), - ("topic2", {"epoch": 4, "y": 2}), - ("topic2", {"epoch": 5, "y": 3}), - ("topic1", {"epoch": 6, "x": 6}), - ("topic1", {"epoch": 7, "x": 7}), - ("topic1", {"epoch": 8, "x": 8}), - EndofData(), - ] - ) - conf = ResamplerConf(resample_rate=1000) - - resamplers = [Resampler(conf, sub1), Resampler(conf, sub2)] - - joiner = Joint(conf, resamplers) - - return_data = await joiner.receive() - assert return_data == {"epoch": 2, "x": 2, "y": 0} - - return_data = await joiner.receive() - assert return_data == {"epoch": 3, "x": 3, "y": 1} diff --git a/tests/test_mqtt.py b/tests/test_mqtt.py deleted file mode 100644 index cd1867d..0000000 --- a/tests/test_mqtt.py +++ /dev/null @@ -1,194 +0,0 @@ -import json -from queue import SimpleQueue -from unittest.mock import call, patch - -import pytest -from paho.mqtt.client import Client, MQTTMessage - -from heisskleber.mqtt.config import MqttConf -from heisskleber.mqtt.mqtt_base import MqttBase -from heisskleber.mqtt.publisher import MqttPublisher -from heisskleber.mqtt.subscriber import MqttSubscriber - - -# Mock configuration for MQTT_Base -@pytest.fixture -def mock_mqtt_conf() -> MqttConf: - return MqttConf( - host="localhost", - port=1883, - user="user", - password="passwd", # noqa: S106, this is a test password - ssl=False, - verbose=False, - qos=1, - ) - - -# Mock the paho mqtt client -@pytest.fixture -def mock_mqtt_client(): - with patch("heisskleber.mqtt.mqtt_base.mqtt_client", spec=Client) as mock: - yield mock - - -@pytest.fixture -def mock_queue(): - with patch("heisskleber.mqtt.subscriber.SimpleQueue", spec=SimpleQueue) as mock: - yield mock - - -def test_mqtt_base_intialization(mock_mqtt_client, mock_mqtt_conf): - """Test that the intialization of the mqtt client is as expected.""" - base = MqttBase(config=mock_mqtt_conf) - base.start() - - mock_mqtt_client.assert_called_once() - mock_mqtt_client.return_value.loop_start.assert_called_once() - mock_client_instance = mock_mqtt_client.return_value - mock_client_instance.username_pw_set.assert_called_with(mock_mqtt_conf.user, mock_mqtt_conf.password) - mock_client_instance.connect.assert_called_with(mock_mqtt_conf.host, mock_mqtt_conf.port) - assert base.client - assert base.client.on_connect == base._on_connect - assert base.client.on_disconnect == base._on_disconnect - assert base.client.on_publish == base._on_publish - assert base.client.on_message == base._on_message - - -def test_mqtt_base_on_connect(mock_mqtt_client, mock_mqtt_conf, capsys): - base = MqttBase(config=mock_mqtt_conf) - base._on_connect(None, None, {}, 0) - captured = capsys.readouterr() - assert f"MQTT node connected to {mock_mqtt_conf.host}:{mock_mqtt_conf.port}" in captured.out - - -def test_mqtt_base_on_disconnect_with_error(mock_mqtt_client, mock_mqtt_conf, capsys): - """Assert that the mqtt client shuts down when disconnect callback is received.""" - base = MqttBase(config=mock_mqtt_conf) - with pytest.raises(SystemExit): - base._on_disconnect(None, None, 1) - captured = capsys.readouterr() - assert "Killing this service" in captured.out - print(captured.out) - - -def test_mqtt_subscribes_single_topic(mock_mqtt_client, mock_mqtt_conf): - """Test that the mqtt client subscribes to a single topic.""" - sub = MqttSubscriber(topics="singleTopic", config=mock_mqtt_conf) - sub.start() - - actual_calls = mock_mqtt_client.return_value.subscribe.call_args_list - assert actual_calls == [call("singleTopic", mock_mqtt_conf.qos)] - - -def test_mqtt_subscribes_multiple_topics(mock_mqtt_client, mock_mqtt_conf): - """Test that the mqtt client subscribes to multiple topics passed as list. - - I would love to do this via parametrization, but the call argument is built differently for single size lists and longer lists. - """ - sub = MqttSubscriber(topics=["multiple1", "multiple2"], config=mock_mqtt_conf) - sub.start() - - actual_calls = mock_mqtt_client.return_value.subscribe.call_args_list - assert actual_calls == [ - call([("multiple1", mock_mqtt_conf.qos), ("multiple2", mock_mqtt_conf.qos)]), - ] - - -def test_mqtt_subscribes_multiple_topics_tuple(mock_mqtt_client, mock_mqtt_conf): - """Test that the mqtt client subscribes to multiple topics passed as tuple.""" - sub = MqttSubscriber(topics=("multiple1", "multiple2"), config=mock_mqtt_conf) - sub.start() - - actual_calls = mock_mqtt_client.return_value.subscribe.call_args_list - assert actual_calls == [ - call([("multiple1", mock_mqtt_conf.qos), ("multiple2", mock_mqtt_conf.qos)]), - ] - - -def create_fake_mqtt_message(topic: bytes, payload: bytes) -> MQTTMessage: - msg = MQTTMessage() - msg.topic = topic - msg.payload = payload - return msg - - -def test_receive_with_message(mock_mqtt_conf: MqttConf, mock_mqtt_client, mock_queue): - """Test the mqtt receive function with fake MQTT messages.""" - topic = b"test/topic" - payload = json.dumps({"key": "value"}).encode() - fake_message = create_fake_mqtt_message(topic, payload) - - mock_queue.return_value.get.side_effect = [fake_message] - subscriber = MqttSubscriber(topics=[topic.decode()], config=mock_mqtt_conf) - - received_topic, received_payload = subscriber.receive() - - assert received_topic == "test/topic" - assert received_payload == {"key": "value"} - - -def test_message_is_put_into_queue(mock_mqtt_conf: MqttConf, mock_mqtt_client, mock_queue): - """Test that values a put into a queue when on_message callback is called.""" - topic = b"test/topic" - payload = json.dumps({"key": "value"}).encode() - fake_message = create_fake_mqtt_message(topic, payload) - - mock_queue.return_value.get.side_effect = [fake_message] - subscriber = MqttSubscriber(topics=[topic.decode()], config=mock_mqtt_conf) - - subscriber._on_message(None, None, fake_message) - - mock_queue.return_value.put.assert_called_once_with(fake_message) - - -def test_message_is_put_into_queue_with_actual_queue(mock_mqtt_conf, mock_mqtt_client): - """Test that the buffering via queue works as expected.""" - topic = b"test/topic" - payload = json.dumps({"key": "value"}).encode() - fake_message = create_fake_mqtt_message(topic, payload) - - # mock_queue.return_value.get.side_effect = [fake_message] - subscriber = MqttSubscriber(topics=[topic.decode()], config=mock_mqtt_conf) - - subscriber._on_message(None, None, fake_message) - - topic, return_dict = subscriber.receive() - - assert topic == "test/topic" - assert return_dict == {"key": "value"} - - -def test_publisher_starts_correctly(mock_mqtt_conf, mock_mqtt_client): - publisher = MqttPublisher(mock_mqtt_conf) - publisher.start() - mock_mqtt_client.return_value.connect.assert_called_once_with("localhost", 1883) - - -def test_publisher_starts_on_send(mock_mqtt_conf, mock_mqtt_client): - publisher = MqttPublisher(mock_mqtt_conf) - publisher.send({"test": 1.0}, "test") - mock_mqtt_client.return_value.connect.assert_called_once_with("localhost", 1883) - - -def test_publisher_starts_only_once(mock_mqtt_conf, mock_mqtt_client): - publisher = MqttPublisher(mock_mqtt_conf) - publisher.start() - publisher.send({"test": 1.0}, "test") - mock_mqtt_client.return_value.connect.assert_called_once_with("localhost", 1883) - - -def test_publisher_not_connected(mock_mqtt_conf, mock_mqtt_client): - publisher = MqttPublisher(mock_mqtt_conf) - assert not publisher.is_connected - publisher.start() - assert publisher.is_connected - mock_mqtt_client.return_value.connect.assert_called_once_with("localhost", 1883) - publisher.stop() - assert not publisher.is_connected - mock_mqtt_client.return_value.loop_stop.assert_called_once() - - -def test_publisher_repr(mock_mqtt_conf): - publisher = MqttPublisher(mock_mqtt_conf) - assert str(publisher) == "MqttPublisher(host=localhost, port=1883)" diff --git a/tests/test_packer.py b/tests/test_packer.py deleted file mode 100644 index 62d5dcc..0000000 --- a/tests/test_packer.py +++ /dev/null @@ -1,35 +0,0 @@ -import json -import pickle -from typing import Any - -import pytest - -from heisskleber.core.packer import get_packer, get_unpacker, serialpacker - - -def test_get_packer() -> None: - assert get_packer("json") == json.dumps - assert get_packer("pickle") == pickle.dumps - assert get_packer("default") == json.dumps - assert get_packer("foobar") == json.dumps - assert get_packer("serial") == serialpacker - - -def test_get_unpacker() -> None: - assert get_unpacker("json") == json.loads - assert get_unpacker("pickle") == pickle.loads - assert get_unpacker("default") == json.loads - assert get_unpacker("foobar") == json.loads - - -@pytest.mark.parametrize( - "message,expected", - [ - ({"hi": 1, "da": 2, "nei": 3}, "1,2,3"), - ({"er": 1, "ma": "ga", "gerd": 3, "jo": 4}, "1,ga,3,4"), - ({"": 1, "ho": 0.0, "lee": 0.1, "shit": 1_000}, "1,0.0,0.1,1000"), - ({"be": 1e6, "li": 1_000}, "1000000.0,1000"), - ], -) -def test_serial_packer_functionality(message: dict[str, Any], expected: str) -> None: - assert serialpacker(message) == expected diff --git a/tests/test_serial.py b/tests/test_serial.py deleted file mode 100644 index 1536ac5..0000000 --- a/tests/test_serial.py +++ /dev/null @@ -1,119 +0,0 @@ -from unittest.mock import Mock, patch - -import pytest -import serial - -from heisskleber.core.packer import serialpacker -from heisskleber.serial.config import SerialConf -from heisskleber.serial.publisher import SerialPublisher -from heisskleber.serial.subscriber import SerialSubscriber - - -@pytest.fixture -def serial_conf(): - return SerialConf(port="/dev/test", baudrate=9600, bytesize=8, verbose=False) - - -@pytest.fixture -def mock_serial_device_subscriber(): - with patch("heisskleber.serial.subscriber.serial.Serial") as mock: - yield mock - - -@pytest.fixture -def mock_serial_device_publisher(): - with patch("heisskleber.serial.publisher.serial.Serial") as mock: - yield mock - - -def test_serial_subscriber_initialization(mock_serial_device_subscriber, serial_conf): - """Test that the SerialSubscriber class initializes correctly. - Mocks the serial.Serial class to avoid opening a serial port.""" - sub = SerialSubscriber( - config=serial_conf, - topic="", - ) - sub.start() - mock_serial_device_subscriber.assert_called_with( - port=serial_conf.port, - baudrate=serial_conf.baudrate, - bytesize=serial_conf.bytesize, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE, - ) - - -def test_serial_subscriber_receive(mock_serial_device_subscriber, serial_conf): - """Test that the SerialSubscriber class calls readline and unpack as expected.""" - subscriber = SerialSubscriber(config=serial_conf, topic="") - subscriber.start() - - # Set up the readline return value - mock_serial_instance = mock_serial_device_subscriber.return_value - mock_serial_instance.readline.return_value = b"test message\n" - - # Set up the unpack function to convert message to dict - unpack_func = Mock(return_value={"data": "test message"}) - subscriber.unpack = unpack_func - - # Call the receive method and assert it behaves as expected - _, payload = subscriber.receive() - - # Was readline called? - mock_serial_instance.readline.assert_called_once() - - # Was unpack called? - assert payload == {"data": "test message"} - unpack_func.assert_called_once_with("test message\n") - - -def test_serial_subscriber_converts_bytes_to_str(): - """Test that the SerialSubscriber class converts bytes to str as expected.""" - with patch("heisskleber.serial.subscriber.serial.Serial") as mock_serial: - subscriber = SerialSubscriber(config=SerialConf(), topic="", custom_unpack=lambda x: x) - subscriber.start() - - # Set the readline method to raise UnicodeError - mock_serial_instance = mock_serial.return_value - mock_serial_instance.readline.side_effect = [b"test message", b"test\x86more"] - - _, payload = subscriber.receive() - assert payload == "test message" - - # Assert that none-unicode is skipped - _, payload = subscriber.receive() - assert payload == "testmore" - - -def test_serial_publisher_initialization(mock_serial_device_publisher, serial_conf): - """Test that the SerialPublisher class initializes correctly. - Mocks the serial.Serial class to avoid opening a serial port.""" - publisher = SerialPublisher(config=serial_conf) - publisher.start() - mock_serial_device_publisher.assert_called_with( - port=serial_conf.port, - baudrate=serial_conf.baudrate, - bytesize=serial_conf.bytesize, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE, - ) - assert publisher.serial_connection - - -def test_serial_publisher_send(mock_serial_device_publisher, serial_conf): - """Test that the SerialPublisher class calls write and pack as expected.""" - publisher = SerialPublisher(config=serial_conf) - - # Set up the readline return value - mock_serial_instance = mock_serial_device_publisher.return_value - mock_serial_instance.readline.return_value = b"test message\n" - - # Set up the pack function to convert dict to comma separated string of values - publisher.pack = serialpacker - - # Call the receive method and assert it behaves as expected - publisher.send({"data": "test message", "more_data": "more message"}, "test") - - # Was write called with encoded payload? - mock_serial_instance.write.assert_called_once_with(b"test message,more message") - mock_serial_instance.flush.assert_called_once() diff --git a/tests/test_streamer.py b/tests/test_streamer.py deleted file mode 100644 index 9049aca..0000000 --- a/tests/test_streamer.py +++ /dev/null @@ -1,105 +0,0 @@ -from datetime import datetime, timedelta -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from heisskleber.mqtt import AsyncMqttSubscriber -from heisskleber.stream import Resampler, ResamplerConf -from heisskleber.stream.resampler import floor_dt, timestamp_generator - - -class EndofData(Exception): - pass - - -# Mocking the MQTT Subscriber -@pytest.fixture -def mock_subscriber(): - mock = MagicMock(spec=AsyncMqttSubscriber) - return mock - - -@pytest.mark.parametrize( - "time_in,expected", - [ - (datetime.fromtimestamp(0.1), datetime.fromtimestamp(0.0)), - (datetime.fromtimestamp(0.6), datetime.fromtimestamp(0.5)), - (datetime.fromtimestamp(0.9), datetime.fromtimestamp(0.5)), - (datetime.fromtimestamp(1.1), datetime.fromtimestamp(1.0)), - ], -) -def test_round_dt(time_in, expected): - delta_t = timedelta(milliseconds=500) - assert floor_dt(time_in, delta_t) == expected - - -@pytest.mark.parametrize( - "start,frequency,expected", - [ - (0.1, 500, [0.25, 0.75, 1.25]), - (0.6, 1000, [0.5, 1.5, 2.5]), - (0.9, 2000, [1.0, 3.0, 5.0]), - (0.04, 50, [0.025, 0.075, 0.125]), - ], -) -def test_timestamp_generator(start, frequency, expected): - """Test the timestamp generator. - Expectation is that it will generate timestamps with the frequency as stride length and - start at the floow value of the start timestamp with half the stride length offset. - """ - generator = timestamp_generator(start, frequency) - for ts, exp in zip(generator, expected): - assert ts == exp - - -@pytest.mark.asyncio -async def test_resampler_multiple_modes(mock_subscriber): - mock_subscriber.receive = AsyncMock( - side_effect=[ - ("topic1", {"epoch": 0.05, "data": 1}), # 1st interval - ("topic1", {"epoch": 0.10, "data": 2}), # 1st interval - ("topic1", {"epoch": 0.90, "data": 3}), # 2nd inverval - ("topic1", {"epoch": 1.10, "data": 4}), # 2nd interval - ("topic1", {"epoch": 1.51, "data": 6}), # 3rd interval - ("topic1", {"epoch": 2.51, "data": 7}), # 4th interval - ("topic1", {"epoch": 4.00, "data": 10}), # 5th interval - EndofData(), - ] - ) - - config = ResamplerConf(resample_rate=1000) - resampler = Resampler(config, mock_subscriber) - - # Test the resample method - resampled_data = [await resampler.receive() for _ in range(3)] - resampler.stop() - - assert resampled_data[0] == {"epoch": 0.0, "data": 1.5} - assert resampled_data[1] == {"epoch": 1.0, "data": 3.5} - assert resampled_data[2] == {"epoch": 2.0, "data": 6} - - -@pytest.mark.asyncio -async def test_resampler_upsampling(mock_subscriber): - mock_subscriber.receive = AsyncMock( - side_effect=[ - ("topic1", {"epoch": 0.0, "data": 1}), # 1st interval - ("topic1", {"epoch": 1.0, "data": 2}), # 2st interval - ("topic1", {"epoch": 2.0, "data": 3}), # 3nd inverval - EndofData(), - ] - ) - - config = ResamplerConf(resample_rate=250) - resampler = Resampler(config, mock_subscriber) - - # Test the resample method - resampled_data = [await resampler.receive() for _ in range(7)] - resampler.stop() - - assert resampled_data[0] == {"epoch": 0.0, "data": 1.0} - assert resampled_data[1] == {"epoch": 0.25, "data": 1.25} - assert resampled_data[2] == {"epoch": 0.5, "data": 1.5} - assert resampled_data[3] == {"epoch": 0.75, "data": 1.75} - assert resampled_data[4] == {"epoch": 1.0, "data": 2.0} - assert resampled_data[5] == {"epoch": 1.25, "data": 2.25} diff --git a/tests/test_udp.py b/tests/test_udp.py deleted file mode 100644 index 6060629..0000000 --- a/tests/test_udp.py +++ /dev/null @@ -1,87 +0,0 @@ -import json -import socket -from unittest.mock import patch - -import pytest - -from heisskleber.udp.config import UdpConf -from heisskleber.udp.publisher import UdpPublisher -from heisskleber.udp.subscriber import UdpSubscriber - - -@pytest.fixture -def mock_socket(): - with patch("heisskleber.udp.publisher.socket.socket") as mock_socket: - yield mock_socket - - -@pytest.fixture -def mock_conf(): - return UdpConf(host="127.0.0.1", port=12345, packer="json") - - -def test_connects_to_socket(mock_socket, mock_conf) -> None: - pub = UdpPublisher(mock_conf) - pub.start() - - # constructor was called - mock_socket.assert_called_with(socket.AF_INET, socket.SOCK_DGRAM) - pub.stop() - - -def test_closes_socket(mock_socket, mock_conf) -> None: - pub = UdpPublisher(mock_conf) - pub.start() - pub.stop() - - # instace was closed - mock_socket.return_value.close.assert_called() - - -def test_packs_and_sends_message(mock_socket, mock_conf) -> None: - pub = UdpPublisher(mock_conf) - - # explicitly define packer to be json.dumps - assert pub.pack == json.dumps - - pub.send({"key": "val", "intkey": 1, "floatkey": 1.0}, "test") - - mock_socket.return_value.sendto.assert_called_with( - b'{"key": "val", "intkey": 1, "floatkey": 1.0, "topic": "test"}', - (str(mock_conf.host), mock_conf.port), - ) - pub.stop() - - -def test_subscriber_receives_message_from_queue(mock_conf) -> None: - sub = UdpSubscriber(mock_conf) - - test_topic, test_data = ("test", {"key": "val", "intkey": 1, "floatkey": 1.0}) - - sub._queue.put((test_topic, test_data)) - - topic, data = sub.receive() - assert test_topic == topic - assert test_data == data - sub.stop() - - -@pytest.fixture -def udp_sub(mock_conf): - sub = UdpSubscriber(mock_conf) - sub.config.port = 12346 # explicitly set port to avoid conflicts - sub.start() - yield sub - sub.stop() - - -def test_sends_message_between_pub_and_sub(udp_sub, mock_conf): - pub = UdpPublisher(mock_conf) - test_data = {"key": "val", "intkey": 1, "floatkey": 1.0} - test_topic = "test_topic" - - # Need to copy the dict, because the publisher will mutate it - pub.send(test_data.copy(), test_topic) - topic, data = udp_sub.receive() - assert test_topic == topic - assert test_data == data diff --git a/tests/test_zmq.py b/tests/test_zmq.py deleted file mode 100644 index 86578ee..0000000 --- a/tests/test_zmq.py +++ /dev/null @@ -1,71 +0,0 @@ -import time -from multiprocessing import Process -from pathlib import Path -from unittest.mock import patch - -import pytest - -from heisskleber import get_sink, get_source -from heisskleber.broker.zmq_broker import zmq_broker -from heisskleber.config import load_config -from heisskleber.zmq.config import ZmqConf -from heisskleber.zmq.publisher import ZmqPublisher -from heisskleber.zmq.subscriber import ZmqSubscriber - - -@pytest.fixture -def start_broker(): - # setup broker - with patch("heisskleber.config.parse.get_config_filepath", return_value=Path("tests/resources/zmq.yaml")): - broker_config = load_config(ZmqConf(), "zmq", read_commandline=False) - broker_process = Process( - target=zmq_broker, - args=(broker_config,), - ) - # start broker - broker_process.start() - - yield broker_process - - broker_process.terminate() - - -def test_config_parses_correctly(): - conf = ZmqConf(protocol="tcp", host="localhost", publisher_port=5555, subscriber_port=5556) - assert conf.protocol == "tcp" - assert conf.host == "localhost" - assert conf.publisher_port == 5555 - assert conf.subscriber_port == 5556 - - assert conf.publisher_address == "tcp://localhost:5555" - assert conf.subscriber_address == "tcp://localhost:5556" - - -def test_instantiate_subscriber(): - conf = ZmqConf(protocol="tcp", host="localhost", publisher_port=5555, subscriber_port=5556) - sub = ZmqSubscriber(conf, "test") - assert sub.config == conf - - -def test_instantiate_publisher(): - conf = ZmqConf(protocol="tcp", host="localhost", publisher_port=5555, subscriber_port=5556) - pub = ZmqPublisher(conf) - assert pub.config == conf - - -def test_send_receive(start_broker): - print("test_send_receive") - topic = "test" - source = get_source("zmq", topic) - source.start() - sink = get_sink("zmq") - sink.start() - time.sleep(1) # this is crucial, otherwise the source might hang - for i in range(10): - message = {"m": i} - sink.send(message, topic) - print(f"sent {topic} {message}") - t, m = source.receive() - print(f"received {t} {m}") - assert t == topic - assert m == {"m": i} diff --git a/tests/stream/test_resampling.py b/tests/udp/__init__.py similarity index 100% rename from tests/stream/test_resampling.py rename to tests/udp/__init__.py diff --git a/tests/udp/test_functional_udp.py b/tests/udp/test_functional_udp.py new file mode 100644 index 0000000..b0a9fc1 --- /dev/null +++ b/tests/udp/test_functional_udp.py @@ -0,0 +1,119 @@ +import asyncio +import json + +import pytest + +from heisskleber.udp import UdpConf, UdpReceiver, UdpSender + + +class MockUdpReceiver: + """Helper class to receive UDP messages for testing.""" + + transport: asyncio.DatagramTransport + protocol: asyncio.DatagramProtocol + + def __init__(self): + self.received_data = [] + + class ReceiverProtocol(asyncio.DatagramProtocol): + def __init__(self, received_data): + self.received_data = received_data + + def connection_made(self, transport): + pass + + def datagram_received(self, data, addr): + self.received_data.append(data) + + async def start(self, host: str, port: int): + """Start the UDP receiver.""" + loop = asyncio.get_running_loop() + self.transport, self.protocol = await loop.create_datagram_endpoint( + lambda: self.ReceiverProtocol(self.received_data), + local_addr=(host, port), + ) + + async def stop(self): + """Stop the UDP receiver.""" + if hasattr(self, "transport"): + self.transport.close() + + +class MockUdpSender: + """Helper class to send UDP messages for testing.""" + + transport: asyncio.DatagramTransport + protocol: asyncio.DatagramProtocol + + def __init__(self): + self.received_data = [] + + class SenderProtocol(asyncio.DatagramProtocol): + def connection_made(self, transport): + pass + + async def start(self, host: str, port: int): + """Start the UDP receiver.""" + loop = asyncio.get_running_loop() + self.transport, self.protocol = await loop.create_datagram_endpoint( + lambda: self.SenderProtocol(), + remote_addr=(host, port), + ) + + async def stop(self): + """Stop the UDP receiver.""" + if hasattr(self, "transport"): + self.transport.close() + + +@pytest.mark.asyncio +async def test_udp_source() -> None: + receiver_host = "127.0.0.1" + receiver_port = 35699 + receiver = UdpReceiver(UdpConf(host=receiver_host, port=receiver_port)) + + try: + await receiver.start() + + sink = MockUdpSender() + try: + await sink.start(receiver_host, receiver_port) + sink.transport.sendto(data=json.dumps({"message": "hi there!"}).encode()) + + data, extra = await receiver.receive() + assert data == {"message": "hi there!"} + finally: + await sink.stop() + finally: + await receiver.stop() + + +@pytest.mark.asyncio +async def test_actual_udp_transport(): + """Test actual UDP communication between sender and receiver.""" + mock_receiver = MockUdpReceiver() + receiver_host = "127.0.0.1" + receiver_port = 45678 + + try: + await mock_receiver.start(receiver_host, receiver_port) + + config = UdpConf(host=receiver_host, port=receiver_port) + sink = UdpSender(config) + + try: + await sink.start() + + test_data = {"message": "Hello, UDP!"} + await sink.send(test_data) + await asyncio.sleep(0.1) + + assert len(mock_receiver.received_data) == 1 + received_bytes = mock_receiver.received_data[0] + assert b'"message": "Hello, UDP!"' in received_bytes + + finally: + await sink.stop() + + finally: + await mock_receiver.stop() diff --git a/tests/udp/test_udp.py b/tests/udp/test_udp.py new file mode 100644 index 0000000..9201c16 --- /dev/null +++ b/tests/udp/test_udp.py @@ -0,0 +1,132 @@ +import asyncio +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from heisskleber.udp.config import UdpConf +from heisskleber.udp.sender import UdpProtocol, UdpSender + + +@pytest.fixture +def udp_config(): + """Fixture providing basic UDP configuration.""" + return UdpConf(host="127.0.0.1", port=54321) + + +@pytest.fixture +def mock_transport(): + """Fixture providing a mock transport.""" + transport = MagicMock(spec=asyncio.DatagramTransport) + transport.is_closing.return_value = False + return transport + + +@pytest.fixture +def udp_sink(udp_config): + """Fixture providing a UDP sink instance.""" + return UdpSender(udp_config) + + +@pytest.mark.asyncio +class TestUdpSink: + """Test suite for UdpSink class.""" + + async def test_init(self, udp_sink, udp_config): + """Test initialization of UdpSink.""" + assert udp_sink.config == udp_config + assert not udp_sink.is_connected + assert callable(udp_sink.pack) + + @patch("asyncio.get_running_loop") + async def test_ensure_connection(self, mock_get_loop, udp_sink, mock_transport): + """Test connection establishment.""" + mock_loop = AsyncMock() + mock_loop.create_datagram_endpoint.return_value = (mock_transport, None) + mock_get_loop.return_value = mock_loop + + await udp_sink._ensure_connection() + + mock_loop.create_datagram_endpoint.assert_called_once() + assert udp_sink.is_connected + assert udp_sink._transport == mock_transport + + @patch("asyncio.get_running_loop") + async def test_ensure_connection_already_connected(self, mock_get_loop, udp_sink, mock_transport): + """Test that _ensure_connection doesn't reconnect if already connected.""" + mock_loop = AsyncMock() + mock_get_loop.return_value = mock_loop + udp_sink.is_connected = True + udp_sink._transport = mock_transport + + await udp_sink._ensure_connection() + + mock_loop.create_datagram_endpoint.assert_not_called() + + async def test_stop(self, udp_sink, mock_transport): + """Test stopping the UDP sink.""" + udp_sink.is_connected = True + udp_sink._transport = mock_transport + + await udp_sink.stop() + + mock_transport.close.assert_called_once() + assert not udp_sink.is_connected + + async def test_stop_not_connected(self, udp_sink: UdpSender) -> None: + """Test stopping when not connected.""" + await udp_sink.stop() + assert not udp_sink.is_connected + + @patch("asyncio.get_running_loop") + async def test_send(self, mock_get_loop, udp_sink, mock_transport): + """Test sending data through UDP sink.""" + mock_loop = AsyncMock() + mock_loop.create_datagram_endpoint.return_value = (mock_transport, None) + mock_get_loop.return_value = mock_loop + + test_data = {"test": "data"} + await udp_sink.send(test_data) + + expected_payload = json.dumps(test_data).encode() + mock_transport.sendto.assert_called_once_with(expected_payload) + + @patch("asyncio.get_running_loop") + async def test_send_custom_packer(self, mock_get_loop, udp_config, mock_transport): + """Test sending data with custom packer.""" + + def custom_packer(data: dict) -> bytes: + return b"custom_packed_data" + + sink = UdpSender(udp_config, packer=custom_packer) + mock_loop = AsyncMock() + mock_loop.create_datagram_endpoint.return_value = (mock_transport, None) + mock_get_loop.return_value = mock_loop + + test_data = {"test": "data"} + await sink.send(test_data) + + mock_transport.sendto.assert_called_once_with(b"custom_packed_data") + await sink.stop() + + +class TestUdpProtocol: + """Test suite for UdpProtocol class.""" + + def test_init(self): + """Test initialization of UdpProtocol.""" + protocol = UdpProtocol(is_connected=True) + assert protocol.is_connected + + def test_connection_lost(self): + """Test connection lost handler.""" + protocol = UdpProtocol(is_connected=True) + protocol.connection_lost(None) + assert not protocol.is_connected + + def test_connection_lost_with_exception(self): + """Test connection lost handler with exception.""" + protocol = UdpProtocol(is_connected=True) + test_exception = Exception("Test exception") + protocol.connection_lost(test_exception) + assert not protocol.is_connected diff --git a/tests/zmq/__init__.py b/tests/zmq/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/zmq/test_zmq.py b/tests/zmq/test_zmq.py new file mode 100644 index 0000000..d1cf56c --- /dev/null +++ b/tests/zmq/test_zmq.py @@ -0,0 +1,23 @@ +import json +from unittest.mock import AsyncMock, Mock, patch + +import pytest + +from heisskleber.zmq import ZmqConf, ZmqSender + + +@pytest.mark.asyncio +async def test_zmq_sink_send() -> None: + mock_socket = AsyncMock() + mock_context = Mock() + mock_context.socket.return_value = mock_socket + + test_dict = {"message": "test"} + test_topic = "test" + + with patch("zmq.asyncio.Context.instance", return_value=mock_context): + zmq_sink = ZmqSender(ZmqConf(publisher_port=5555)) + await zmq_sink.send(test_dict, topic=test_topic) + + mock_socket.connect.assert_called_once_with(zmq_sink.config.publisher_address) + mock_socket.send_multipart.assert_called_once_with([test_topic.encode(), json.dumps(test_dict).encode()]) diff --git a/tests/zmq/test_zmq_asyncio.py b/tests/zmq/test_zmq_asyncio.py deleted file mode 100644 index eb48547..0000000 --- a/tests/zmq/test_zmq_asyncio.py +++ /dev/null @@ -1,65 +0,0 @@ -import time -from collections.abc import Generator -from multiprocessing import Process -from pathlib import Path -from unittest.mock import patch - -import pytest - -from heisskleber.broker.zmq_broker import zmq_broker -from heisskleber.config import load_config -from heisskleber.zmq.config import ZmqConf -from heisskleber.zmq.publisher import ZmqAsyncPublisher, ZmqPublisher -from heisskleber.zmq.subscriber import ZmqAsyncSubscriber - - -@pytest.fixture -def start_broker() -> Generator[Process, None, None]: - # setup broker - with patch( - "heisskleber.config.parse.get_config_filepath", - return_value=Path("tests/resources/zmq.yaml"), - ): - broker_config = load_config(ZmqConf(), "zmq", read_commandline=False) - broker_process = Process( - target=zmq_broker, - args=(broker_config,), - ) - # start broker - broker_process.start() - - yield broker_process - - broker_process.terminate() - - -def test_instantiate_subscriber() -> None: - conf = ZmqConf(protocol="tcp", host="localhost", publisher_port=5555, subscriber_port=5556) - sub = ZmqAsyncSubscriber(conf, "test") - assert sub.config == conf - - -def test_instantiate_publisher() -> None: - conf = ZmqConf(protocol="tcp", host="localhost", publisher_port=5555, subscriber_port=5556) - pub = ZmqPublisher(conf) - assert pub.config == conf - - -@pytest.mark.asyncio -async def test_send_receive(start_broker) -> None: - print("test_send_receive") - topic = "test" - conf = ZmqConf(protocol="tcp", host="localhost", publisher_port=5555, subscriber_port=5556) - source = ZmqAsyncSubscriber(conf, topic) - sink = ZmqAsyncPublisher(conf) - source.start() - sink.start() - time.sleep(1) # this is crucial, otherwise the source might hang - for i in range(10): - message = {"m": i} - await sink.send(message, topic) - print(f"sent {topic} {message}") - t, m = await source.receive() - print(f"received {t} {m}") - assert t == topic - assert m == {"m": i} diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..82e4982 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1370 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "aiomqtt" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "paho-mqtt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/c9/168e78bd35b21d9bdbb26178db33a8f265e4a69bb4193e72434e7cb3d1cd/aiomqtt-2.3.0.tar.gz", hash = "sha256:312feebe20bc76dc7c20916663011f3bd37aa6f42f9f687a19a1c58308d80d47", size = 16479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/5f/73aa0474a75fce798c8c45a0993720c3722688ae5bea1d0a5c4a9f97ae8e/aiomqtt-2.3.0-py3-none-any.whl", hash = "sha256:127926717bd6b012d1630f9087f24552eb9c4af58205bc2964f09d6e304f7e63", size = 15807 }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, +] + +[[package]] +name = "anyio" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/49/f3f17ec11c4a91fe79275c426658e509b07547f874b14c1a526d86a83fc8/anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb", size = 170983 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/ef/7a4f225581a0d7886ea28359179cb861d7fbcdefad29663fc1167b86f69f/anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a", size = 89631 }, +] + +[[package]] +name = "argcomplete" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/33/a3d23a2e9ac78f9eaf1fce7490fee430d43ca7d42c65adabbb36a2b28ff6/argcomplete-3.5.0.tar.gz", hash = "sha256:4349400469dccfb7950bb60334a680c58d88699bff6159df61251878dc6bf74b", size = 82237 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/e8/ba56bcc0d48170c0fc5a7f389488eddce47f98ed976a24ae62db402f33ae/argcomplete-3.5.0-py3-none-any.whl", hash = "sha256:d4bcf3ff544f51e16e54228a7ac7f486ed70ebf2ecfe49a63a91171c76bf029b", size = 43475 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/ca/824b1195773ce6166d388573fc106ce56d4a805bd7427b624e063596ec58/beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", size = 581181 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed", size = 147925 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", size = 194219 }, + { url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", size = 122521 }, + { url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", size = 120383 }, + { url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", size = 138223 }, + { url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", size = 148101 }, + { url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", size = 140699 }, + { url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", size = 142065 }, + { url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", size = 144505 }, + { url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", size = 139425 }, + { url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", size = 145287 }, + { url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", size = 149929 }, + { url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", size = 141605 }, + { url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", size = 142646 }, + { url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", size = 92846 }, + { url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", size = 100343 }, + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "colorlog" +version = "6.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/38/2992ff192eaa7dd5a793f8b6570d6bbe887c4fbbf7e72702eb0a693a01c8/colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44", size = 16529 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/18/3e867ab37a24fdf073c1617b9c7830e06ec270b1ea4694a624038fc40a03/colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33", size = 11357 }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } +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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "deptry" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/9e/7a976d923d3ae18d7dc4ace8e0c83e20a847828196e7f4b13a4bf6b03b50/deptry-0.20.0.tar.gz", hash = "sha256:62e9aaf3aea9e2ca66c85da98a0ba0290b4d3daea4e1d0ad937d447bd3c36402", size = 129936 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/da/c94ebc2192a29a6f45acb5b87fdb31d1b84843154572d9b88100b7047eda/deptry-0.20.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:41434d95124851b83cb05524d1a09ad6fea62006beafed2ef90a6b501c1b237f", size = 1624964 }, + { url = "https://files.pythonhosted.org/packages/98/8e/08f7b33b384a7981b27de5aa3def41b6fa691aa692904910dc1f5bd1fc02/deptry-0.20.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:b3b4b22d1406147de5d606a24042126cd74d52fdfdb0232b9c5fd0270d601610", size = 1545726 }, + { url = "https://files.pythonhosted.org/packages/55/47/8e813609a4ba6c75032bd3468f9edcad31e11906eafd0a1e5a3f3f837fba/deptry-0.20.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012fb106dbea6ca95196cdcd75ac90c516c8f01292f7934f2e802a7cf025a660", size = 1676818 }, + { url = "https://files.pythonhosted.org/packages/b4/70/456d976912c6026252034c0cdb37a3cbad34ac0ce815763466720c63aece/deptry-0.20.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ce3920e2bd6d2b4427ab31ab8efb94bbef897001c2d395782bc30002966d12d", size = 1708051 }, + { url = "https://files.pythonhosted.org/packages/ff/66/95e04a84120861b0c0ac980999e6172612509d5ff9a84b41e2f71cc3c3c0/deptry-0.20.0-cp38-abi3-win_amd64.whl", hash = "sha256:0c90ce64e637d0e902bc97c5a020adecfee9e9f09ee0bf4c61554994139bebdb", size = 1493281 }, + { url = "https://files.pythonhosted.org/packages/53/c9/9d7d86b5fdc452b522ef16df9e27c8404dc6f231fa865a3af31c1dab7563/deptry-0.20.0-cp38-abi3-win_arm64.whl", hash = "sha256:6886ff44aaf26fd83093f14f844ebc84589d90df9bbad9a1625e8a080e6f1be2", size = 1420087 }, + { url = "https://files.pythonhosted.org/packages/2a/06/57ccbad1a66e9a17980f03f6aed9724577a5acd58c761ede76e4b03004a7/deptry-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ace3b39b1d0763f357c79bab003d1b135bea2eb61102be539992621a42d1ac7b", size = 1624520 }, + { url = "https://files.pythonhosted.org/packages/d9/00/c8b214f4a0c52b95cabb35197046efc84f9205eeef1d12026e865eeab373/deptry-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d1a00f8c9e6c0829a4a523edd5e526e3df06d2b50e0a99446f09f9723df2efad", size = 1545283 }, + { url = "https://files.pythonhosted.org/packages/c6/6f/999f8cdb338cceb48e2d05e9638f988cd25d4971d1882e251691ecd41fa0/deptry-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e233859f150df70ffff76e95f9b7326fc25494b9beb26e776edae20f0f515e7d", size = 1677736 }, + { url = "https://files.pythonhosted.org/packages/a0/06/2fffc44168e139619c83de0a2af293c88c08879b93de72b3041a3b4e0eed/deptry-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f92e7e97ef42477717747b190bc6796ab94b35655af126d8c577f7eae0eb3a9", size = 1707537 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/f5465abf491f945175d60f4a52f5c1b8bec7d58bfce41a6dc5d5894fc7b3/deptry-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f6cee6005997791bb77155667be055333fb63ae9a24f0f103f25faf1e7affe34", size = 1493191 }, +] + +[[package]] +name = "distlib" +version = "0.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "furo" +version = "2024.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "sphinx-basic-ng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "heisskleber" +version = "1.0.0.dev6+g6b5270f.d20241118" +source = { editable = "." } +dependencies = [ + { name = "aiomqtt" }, + { name = "pyserial" }, + { name = "pyyaml" }, + { name = "pyzmq" }, +] + +[package.optional-dependencies] +docs = [ + { name = "furo" }, + { name = "myst-parser" }, + { name = "sphinx" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-autodoc-typehints" }, + { name = "sphinx-copybutton" }, + { name = "sphinx-rtd-theme" }, +] +filter = [ + { name = "numpy" }, + { name = "scipy" }, +] +test = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "xdoctest" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage", extra = ["toml"] }, + { name = "deptry" }, + { name = "mypy" }, + { name = "nox" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "xdoctest" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiomqtt", specifier = ">=2.3.0" }, + { name = "coverage", extras = ["toml"], marker = "extra == 'test'", specifier = ">=7.6.1" }, + { name = "furo", marker = "extra == 'docs'", specifier = ">=2024.8.6" }, + { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=4.0.0" }, + { name = "numpy", marker = "extra == 'filter'", specifier = ">=2.1.1" }, + { name = "pyserial", specifier = ">=3.5" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.3" }, + { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.24.0" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0.0" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "pyzmq", specifier = ">=26.2.0" }, + { name = "scipy", marker = "extra == 'filter'", specifier = ">=1.14.1" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=8.0.2" }, + { name = "sphinx-autobuild", marker = "extra == 'docs'", specifier = ">=2024.9.19" }, + { name = "sphinx-autodoc-typehints", marker = "extra == 'docs'" }, + { name = "sphinx-copybutton", marker = "extra == 'docs'" }, + { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=0.5.1" }, + { name = "xdoctest", marker = "extra == 'test'", specifier = ">=1.2.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", extras = ["toml"], specifier = ">=7.6.1" }, + { name = "deptry", specifier = ">=0.20.0" }, + { name = "mypy", specifier = ">=1.11.2" }, + { name = "nox", specifier = ">=2024.4.15" }, + { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-asyncio", specifier = ">=0.24.0" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "ruff", specifier = ">=0.6.8" }, + { name = "xdoctest", specifier = ">=1.2.0" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206 }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620 }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818 }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493 }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630 }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745 }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021 }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659 }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213 }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mypy" +version = "1.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/cd/815368cd83c3a31873e5e55b317551500b12f2d1d7549720632f32630333/mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", size = 10939401 }, + { url = "https://files.pythonhosted.org/packages/f1/27/e18c93a195d2fad75eb96e1f1cbc431842c332e8eba2e2b77eaf7313c6b7/mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", size = 10111697 }, + { url = "https://files.pythonhosted.org/packages/dc/08/cdc1fc6d0d5a67d354741344cc4aa7d53f7128902ebcbe699ddd4f15a61c/mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", size = 12500508 }, + { url = "https://files.pythonhosted.org/packages/64/12/aad3af008c92c2d5d0720ea3b6674ba94a98cdb86888d389acdb5f218c30/mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", size = 13020712 }, + { url = "https://files.pythonhosted.org/packages/03/e6/a7d97cc124a565be5e9b7d5c2a6ebf082379ffba99646e4863ed5bbcb3c3/mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", size = 9567319 }, + { url = "https://files.pythonhosted.org/packages/e2/aa/cc56fb53ebe14c64f1fe91d32d838d6f4db948b9494e200d2f61b820b85d/mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", size = 10859630 }, + { url = "https://files.pythonhosted.org/packages/04/c8/b19a760fab491c22c51975cf74e3d253b8c8ce2be7afaa2490fbf95a8c59/mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", size = 10037973 }, + { url = "https://files.pythonhosted.org/packages/88/57/7e7e39f2619c8f74a22efb9a4c4eff32b09d3798335625a124436d121d89/mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", size = 12416659 }, + { url = "https://files.pythonhosted.org/packages/fc/a6/37f7544666b63a27e46c48f49caeee388bf3ce95f9c570eb5cfba5234405/mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", size = 12897010 }, + { url = "https://files.pythonhosted.org/packages/84/8b/459a513badc4d34acb31c736a0101c22d2bd0697b969796ad93294165cfb/mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", size = 9562873 }, + { url = "https://files.pythonhosted.org/packages/35/3a/ed7b12ecc3f6db2f664ccf85cb2e004d3e90bec928e9d7be6aa2f16b7cdf/mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", size = 10990335 }, + { url = "https://files.pythonhosted.org/packages/04/e4/1a9051e2ef10296d206519f1df13d2cc896aea39e8683302f89bf5792a59/mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", size = 10007119 }, + { url = "https://files.pythonhosted.org/packages/f3/3c/350a9da895f8a7e87ade0028b962be0252d152e0c2fbaafa6f0658b4d0d4/mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", size = 12506856 }, + { url = "https://files.pythonhosted.org/packages/b6/49/ee5adf6a49ff13f4202d949544d3d08abb0ea1f3e7f2a6d5b4c10ba0360a/mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", size = 12952066 }, + { url = "https://files.pythonhosted.org/packages/27/c0/b19d709a42b24004d720db37446a42abadf844d5c46a2c442e2a074d70d9/mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", size = 9664000 }, + { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "myst-parser" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/55/6d1741a1780e5e65038b74bce6689da15f620261c490c3511eb4c12bac4b/myst_parser-4.0.0.tar.gz", hash = "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531", size = 93858 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/b4/b036f8fdb667587bb37df29dc6644681dd78b7a2a6321a34684b79412b28/myst_parser-4.0.0-py3-none-any.whl", hash = "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d", size = 84563 }, +] + +[[package]] +name = "nox" +version = "2024.4.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "colorlog" }, + { name = "packaging" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/86/b86fc26784d2f63d038b4efc9e18d4d807ec025569da66c6d032b8f717df/nox-2024.4.15.tar.gz", hash = "sha256:ecf6700199cdfa9e5ea0a41ff5e6ef4641d09508eda6edb89d9987864115817f", size = 4000608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/28/2897c06b54cd99f41ca9e5cc7433211a085903a71aaed1cb1a1dc138d53c/nox-2024.4.15-py3-none-any.whl", hash = "sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565", size = 60719 }, +] + +[[package]] +name = "numpy" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/5f/9003bb3e632f2b58f5e3a3378902dcc73c5518070736c6740fe52454e8e1/numpy-2.1.1.tar.gz", hash = "sha256:d0cf7d55b1051387807405b3898efafa862997b4cba8aa5dbe657be794afeafd", size = 18874860 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/37/e3de47233b3ba458b1021a6f95029198b2f68a83eb886a862640b6ec3e9a/numpy-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8a0e34993b510fc19b9a2ce7f31cb8e94ecf6e924a40c0c9dd4f62d0aac47d9", size = 21150738 }, + { url = "https://files.pythonhosted.org/packages/69/30/f41c9b6dab4e1ec56b40d1daa81ce9f9f8d26da6d02af18768a883676bd5/numpy-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dd86dfaf7c900c0bbdcb8b16e2f6ddf1eb1fe39c6c8cca6e94844ed3152a8fd", size = 13758247 }, + { url = "https://files.pythonhosted.org/packages/e1/30/d2f71d3419ada3b3735e2ce9cea7dfe22c268ac9fbb24e0b5ac5fc222633/numpy-2.1.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:5889dd24f03ca5a5b1e8a90a33b5a0846d8977565e4ae003a63d22ecddf6782f", size = 5353756 }, + { url = "https://files.pythonhosted.org/packages/84/64/879bd6877488441cfaa578c96bdc4b43710d7e3ae4f8260fbd04821da395/numpy-2.1.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:59ca673ad11d4b84ceb385290ed0ebe60266e356641428c845b39cd9df6713ab", size = 6886809 }, + { url = "https://files.pythonhosted.org/packages/cd/c4/869f8db87f5c9df86b93ca42036f58911ff162dd091a41e617977ab50d1f/numpy-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ce49a34c44b6de5241f0b38b07e44c1b2dcacd9e36c30f9c2fcb1bb5135db7", size = 13977367 }, + { url = "https://files.pythonhosted.org/packages/7d/4b/a509d346fffede6120cc17610cc500819417ee9c3da7f08d9aaf15cab2a3/numpy-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913cc1d311060b1d409e609947fa1b9753701dac96e6581b58afc36b7ee35af6", size = 16326516 }, + { url = "https://files.pythonhosted.org/packages/4a/0c/fdba41b2ddeb7a052f84d85fb17d5e168af0e8034b3a2d6e369b7cc2966f/numpy-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:caf5d284ddea7462c32b8d4a6b8af030b6c9fd5332afb70e7414d7fdded4bfd0", size = 16702642 }, + { url = "https://files.pythonhosted.org/packages/bf/8d/a8da065a46515efdbcf81a92535b816ea17194ce5b767df1f13815c32179/numpy-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:57eb525e7c2a8fdee02d731f647146ff54ea8c973364f3b850069ffb42799647", size = 14475522 }, + { url = "https://files.pythonhosted.org/packages/b9/d2/5b7cf5851af48c35a73b85750b41f9b622760ee11659665a688e6b3f7cb7/numpy-2.1.1-cp310-cp310-win32.whl", hash = "sha256:9a8e06c7a980869ea67bbf551283bbed2856915f0a792dc32dd0f9dd2fb56728", size = 6535211 }, + { url = "https://files.pythonhosted.org/packages/e5/6a/b1f7d73fec1942ded4b474a78c3fdd11c4fad5232143f41dd7e6ae166080/numpy-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d10c39947a2d351d6d466b4ae83dad4c37cd6c3cdd6d5d0fa797da56f710a6ae", size = 12865289 }, + { url = "https://files.pythonhosted.org/packages/f7/86/2c01070424a42b286ea0271203682c3d3e81e10ce695545b35768307b383/numpy-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d07841fd284718feffe7dd17a63a2e6c78679b2d386d3e82f44f0108c905550", size = 21154850 }, + { url = "https://files.pythonhosted.org/packages/ef/4e/d3426d9e620a18bbb979f28e4dc7f9a2c35eb7cf726ffcb33545ebdd3e6a/numpy-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5613cfeb1adfe791e8e681128f5f49f22f3fcaa942255a6124d58ca59d9528f", size = 13789477 }, + { url = "https://files.pythonhosted.org/packages/c6/6e/fb6b1b2da9f4c757f55b202f10b6af0fe4fee87ace6e830228a12ab8ae5d/numpy-2.1.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0b8cc2715a84b7c3b161f9ebbd942740aaed913584cae9cdc7f8ad5ad41943d0", size = 5351769 }, + { url = "https://files.pythonhosted.org/packages/58/9a/07c8a9dc7254f3265ae014e33768d1cfd8eb73ee6cf215f4ec3b497e4255/numpy-2.1.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b49742cdb85f1f81e4dc1b39dcf328244f4d8d1ded95dea725b316bd2cf18c95", size = 6890872 }, + { url = "https://files.pythonhosted.org/packages/08/4e/3b50fa3b1e045793056ed5a1fc6f89dd897ff9cb00900ca6377fe552d442/numpy-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8d5f8a8e3bc87334f025194c6193e408903d21ebaeb10952264943a985066ca", size = 13984256 }, + { url = "https://files.pythonhosted.org/packages/d9/37/108d692f7e2544b9ae972c7bfa06c26717871c273ccec86470bc3132b04d/numpy-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d51fc141ddbe3f919e91a096ec739f49d686df8af254b2053ba21a910ae518bf", size = 16337778 }, + { url = "https://files.pythonhosted.org/packages/95/2d/df81a1be3be6d3a92fd12dfd6c26a0dc026b276136ec1056562342a484a2/numpy-2.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:98ce7fb5b8063cfdd86596b9c762bf2b5e35a2cdd7e967494ab78a1fa7f8b86e", size = 16710448 }, + { url = "https://files.pythonhosted.org/packages/8f/34/4b2e604c5c44bd64b6c85e89d88871b41e60233b3ddf97419b37ae5b0c72/numpy-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:24c2ad697bd8593887b019817ddd9974a7f429c14a5469d7fad413f28340a6d2", size = 14489002 }, + { url = "https://files.pythonhosted.org/packages/9f/0d/67c04b6bfefd0abbe7f60f7e4f11e3aca15d688faec1d1df089966105a9a/numpy-2.1.1-cp311-cp311-win32.whl", hash = "sha256:397bc5ce62d3fb73f304bec332171535c187e0643e176a6e9421a6e3eacef06d", size = 6533215 }, + { url = "https://files.pythonhosted.org/packages/94/7a/4c00332a3ca79702bbc86228afd0e84e6f91b47222ec8cdf00677dd16481/numpy-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:ae8ce252404cdd4de56dcfce8b11eac3c594a9c16c231d081fb705cf23bd4d9e", size = 12870550 }, + { url = "https://files.pythonhosted.org/packages/36/11/c573ef66c004f991989c2c6218229d9003164525549409aec5ec9afc0285/numpy-2.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c803b7934a7f59563db459292e6aa078bb38b7ab1446ca38dd138646a38203e", size = 20884403 }, + { url = "https://files.pythonhosted.org/packages/6b/6c/a9fbef5fd2f9685212af2a9e47485cde9357c3e303e079ccf85127516f2d/numpy-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6435c48250c12f001920f0751fe50c0348f5f240852cfddc5e2f97e007544cbe", size = 13493375 }, + { url = "https://files.pythonhosted.org/packages/34/f2/1316a6b08ad4c161d793abe81ff7181e9ae2e357a5b06352a383b9f8e800/numpy-2.1.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3269c9eb8745e8d975980b3a7411a98976824e1fdef11f0aacf76147f662b15f", size = 5088823 }, + { url = "https://files.pythonhosted.org/packages/be/15/fabf78a6d4a10c250e87daf1cd901af05e71501380532ac508879cc46a7e/numpy-2.1.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fac6e277a41163d27dfab5f4ec1f7a83fac94e170665a4a50191b545721c6521", size = 6619825 }, + { url = "https://files.pythonhosted.org/packages/9f/8a/76ddef3e621541ddd6984bc24d256a4e3422d036790cbbe449e6cad439ee/numpy-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcd8f556cdc8cfe35e70efb92463082b7f43dd7e547eb071ffc36abc0ca4699b", size = 13696705 }, + { url = "https://files.pythonhosted.org/packages/cb/22/2b840d297183916a95847c11f82ae11e248fa98113490b2357f774651e1d/numpy-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b9cd92c8f8e7b313b80e93cedc12c0112088541dcedd9197b5dee3738c1201", size = 16041649 }, + { url = "https://files.pythonhosted.org/packages/c7/e8/6f4825d8f576cfd5e4d6515b9eec22bd618868bdafc8a8c08b446dcb65f0/numpy-2.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:afd9c680df4de71cd58582b51e88a61feed4abcc7530bcd3d48483f20fc76f2a", size = 16409358 }, + { url = "https://files.pythonhosted.org/packages/bf/f8/5edf1105b0dc24fd66fc3e9e7f3bca3d920cde571caaa4375ec1566073c3/numpy-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8661c94e3aad18e1ea17a11f60f843a4933ccaf1a25a7c6a9182af70610b2313", size = 14172488 }, + { url = "https://files.pythonhosted.org/packages/f4/c2/dddca3e69a024d2f249a5b68698328163cbdafb7e65fbf6d36373bbabf12/numpy-2.1.1-cp312-cp312-win32.whl", hash = "sha256:950802d17a33c07cba7fd7c3dcfa7d64705509206be1606f196d179e539111ed", size = 6237195 }, + { url = "https://files.pythonhosted.org/packages/b7/98/5640a09daa3abf0caeaefa6e7bf0d10c0aa28a77c84e507d6a716e0e23df/numpy-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:3fc5eabfc720db95d68e6646e88f8b399bfedd235994016351b1d9e062c4b270", size = 12568082 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/8bc6f133bc6d359ccc9ec051853aded45504d217685191f31f46d36b7065/numpy-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:046356b19d7ad1890c751b99acad5e82dc4a02232013bd9a9a712fddf8eb60f5", size = 20834810 }, + { url = "https://files.pythonhosted.org/packages/32/1b/429519a2fa28681814c511574017d35f3aab7136d554cc65f4c1526dfbf5/numpy-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6e5a9cb2be39350ae6c8f79410744e80154df658d5bea06e06e0ac5bb75480d5", size = 13507739 }, + { url = "https://files.pythonhosted.org/packages/25/18/c732d7dd9896d11e4afcd487ac65e62f9fa0495563b7614eb850765361fa/numpy-2.1.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d4c57b68c8ef5e1ebf47238e99bf27657511ec3f071c465f6b1bccbef12d4136", size = 5074465 }, + { url = "https://files.pythonhosted.org/packages/3e/37/838b7ae9262c370ab25312bab365492016f11810ffc03ebebbd54670b669/numpy-2.1.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:8ae0fd135e0b157365ac7cc31fff27f07a5572bdfc38f9c2d43b2aff416cc8b0", size = 6606418 }, + { url = "https://files.pythonhosted.org/packages/8b/b9/7ff3bfb71e316a5b43a124c4b7a5881ab12f3c32636014bef1f757f19dbd/numpy-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981707f6b31b59c0c24bcda52e5605f9701cb46da4b86c2e8023656ad3e833cb", size = 13692464 }, + { url = "https://files.pythonhosted.org/packages/42/78/75bcf16e6737cd196ff7ecf0e1fd3f953293a34dff4fd93fb488e8308536/numpy-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ca4b53e1e0b279142113b8c5eb7d7a877e967c306edc34f3b58e9be12fda8df", size = 16037763 }, + { url = "https://files.pythonhosted.org/packages/23/99/36bf5ffe034d06df307bc783e25cf164775863166dcd878879559fe0379f/numpy-2.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e097507396c0be4e547ff15b13dc3866f45f3680f789c1a1301b07dadd3fbc78", size = 16410374 }, + { url = "https://files.pythonhosted.org/packages/7f/16/04c5dab564887d4cd31a9ed30e51467fa70d52a4425f5a9bd1eed5b3d34c/numpy-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7506387e191fe8cdb267f912469a3cccc538ab108471291636a96a54e599556", size = 14169873 }, + { url = "https://files.pythonhosted.org/packages/09/e0/d1b5adbf1731886c4186c59a9fa208585df9452a43a2b60e79af7c649717/numpy-2.1.1-cp313-cp313-win32.whl", hash = "sha256:251105b7c42abe40e3a689881e1793370cc9724ad50d64b30b358bbb3a97553b", size = 6234118 }, + { url = "https://files.pythonhosted.org/packages/d0/9c/2391ee6e9ebe77232ddcab29d92662b545e99d78c3eb3b4e26d59b9ca1ca/numpy-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:f212d4f46b67ff604d11fff7cc62d36b3e8714edf68e44e9760e19be38c03eb0", size = 12561742 }, + { url = "https://files.pythonhosted.org/packages/38/0e/c4f754f9e73f9bb520e8bf418c646f2c4f70c5d5f2bc561e90f884593193/numpy-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:920b0911bb2e4414c50e55bd658baeb78281a47feeb064ab40c2b66ecba85553", size = 20858403 }, + { url = "https://files.pythonhosted.org/packages/32/fc/d69092b9171efa0cb8079577e71ce0cac0e08f917d33f6e99c916ed51d44/numpy-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bab7c09454460a487e631ffc0c42057e3d8f2a9ddccd1e60c7bb8ed774992480", size = 13519851 }, + { url = "https://files.pythonhosted.org/packages/14/2a/d7cf2cd9f15b23f623075546ea64a2c367cab703338ca22aaaecf7e704df/numpy-2.1.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:cea427d1350f3fd0d2818ce7350095c1a2ee33e30961d2f0fef48576ddbbe90f", size = 5115444 }, + { url = "https://files.pythonhosted.org/packages/8e/00/e87b2cb4afcecca3b678deefb8fa53005d7054f3b5c39596e5554e5d98f8/numpy-2.1.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:e30356d530528a42eeba51420ae8bf6c6c09559051887196599d96ee5f536468", size = 6628903 }, + { url = "https://files.pythonhosted.org/packages/ab/9d/337ae8721b3beec48c3413d71f2d44b2defbf3c6f7a85184fc18b7b61f4a/numpy-2.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8dfa9e94fc127c40979c3eacbae1e61fda4fe71d84869cc129e2721973231ef", size = 13665945 }, + { url = "https://files.pythonhosted.org/packages/c0/90/ee8668e84c5d5cc080ef3beb622c016adf19ca3aa51afe9dbdcc6a9baf59/numpy-2.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910b47a6d0635ec1bd53b88f86120a52bf56dcc27b51f18c7b4a2e2224c29f0f", size = 16023473 }, + { url = "https://files.pythonhosted.org/packages/38/a0/57c24b2131879183051dc698fbb53fd43b77c3fa85b6e6311014f2bc2973/numpy-2.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:13cc11c00000848702322af4de0147ced365c81d66053a67c2e962a485b3717c", size = 16400624 }, + { url = "https://files.pythonhosted.org/packages/bb/4c/14a41eb5c9548c6cee6af0936eabfd985c69230ffa2f2598321431a9aa0a/numpy-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53e27293b3a2b661c03f79aa51c3987492bd4641ef933e366e0f9f6c9bf257ec", size = 14155072 }, + { url = "https://files.pythonhosted.org/packages/94/9a/d6a5d138b53ccdc002fdf07f0d1a960326c510e66cbfff7180c88d37c482/numpy-2.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7be6a07520b88214ea85d8ac8b7d6d8a1839b0b5cb87412ac9f49fa934eb15d5", size = 20982055 }, + { url = "https://files.pythonhosted.org/packages/40/b5/78d8b5481aeef6d2aad3724c6aa5398045d2657038dfe54c055cae1fcf75/numpy-2.1.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:52ac2e48f5ad847cd43c4755520a2317f3380213493b9d8a4c5e37f3b87df504", size = 6750222 }, + { url = "https://files.pythonhosted.org/packages/eb/9a/59a548ad57df8c432bfac4556504a9fae5c082ffea53d108fcf7ce2956e4/numpy-2.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a95ca3560a6058d6ea91d4629a83a897ee27c00630aed9d933dff191f170cd", size = 16141236 }, + { url = "https://files.pythonhosted.org/packages/02/31/3cbba87e998748b2e33ca5bc6fcc5662c867037f980918e302aebdf139a2/numpy-2.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:99f4a9ee60eed1385a86e82288971a51e71df052ed0b2900ed30bc840c0f2e39", size = 12789681 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "paho-mqtt" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pyserial" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "pyzmq" +version = "26.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/05/bed626b9f7bb2322cdbbf7b4bd8f54b1b617b0d2ab2d3547d6e39428a48e/pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", size = 271975 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/a8/9837c39aba390eb7d01924ace49d761c8dbe7bc2d6082346d00c8332e431/pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629", size = 1340058 }, + { url = "https://files.pythonhosted.org/packages/a2/1f/a006f2e8e4f7d41d464272012695da17fb95f33b54342612a6890da96ff6/pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b", size = 1008818 }, + { url = "https://files.pythonhosted.org/packages/b6/09/b51b6683fde5ca04593a57bbe81788b6b43114d8f8ee4e80afc991e14760/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764", size = 673199 }, + { url = "https://files.pythonhosted.org/packages/c9/78/486f3e2e824f3a645238332bf5a4c4b4477c3063033a27c1e4052358dee2/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c", size = 911762 }, + { url = "https://files.pythonhosted.org/packages/5e/3b/2eb1667c9b866f53e76ee8b0c301b0469745a23bd5a87b7ee3d5dd9eb6e5/pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a", size = 868773 }, + { url = "https://files.pythonhosted.org/packages/16/29/ca99b4598a9dc7e468b5417eda91f372b595be1e3eec9b7cbe8e5d3584e8/pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88", size = 868834 }, + { url = "https://files.pythonhosted.org/packages/ad/e5/9efaeb1d2f4f8c50da04144f639b042bc52869d3a206d6bf672ab3522163/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f", size = 1202861 }, + { url = "https://files.pythonhosted.org/packages/c3/62/c721b5608a8ac0a69bb83cbb7d07a56f3ff00b3991a138e44198a16f94c7/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282", size = 1515304 }, + { url = "https://files.pythonhosted.org/packages/87/84/e8bd321aa99b72f48d4606fc5a0a920154125bd0a4608c67eab742dab087/pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea", size = 1414712 }, + { url = "https://files.pythonhosted.org/packages/cd/cd/420e3fd1ac6977b008b72e7ad2dae6350cc84d4c5027fc390b024e61738f/pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2", size = 578113 }, + { url = "https://files.pythonhosted.org/packages/5c/57/73930d56ed45ae0cb4946f383f985c855c9b3d4063f26416998f07523c0e/pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971", size = 641631 }, + { url = "https://files.pythonhosted.org/packages/61/d2/ae6ac5c397f1ccad59031c64beaafce7a0d6182e0452cc48f1c9c87d2dd0/pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa", size = 543528 }, + { url = "https://files.pythonhosted.org/packages/12/20/de7442172f77f7c96299a0ac70e7d4fb78cd51eca67aa2cf552b66c14196/pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218", size = 1340639 }, + { url = "https://files.pythonhosted.org/packages/98/4d/5000468bd64c7910190ed0a6c76a1ca59a68189ec1f007c451dc181a22f4/pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4", size = 1008710 }, + { url = "https://files.pythonhosted.org/packages/e1/bf/c67fd638c2f9fbbab8090a3ee779370b97c82b84cc12d0c498b285d7b2c0/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef", size = 673129 }, + { url = "https://files.pythonhosted.org/packages/86/94/99085a3f492aa538161cbf27246e8886ff850e113e0c294a5b8245f13b52/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317", size = 910107 }, + { url = "https://files.pythonhosted.org/packages/31/1d/346809e8a9b999646d03f21096428453465b1bca5cd5c64ecd048d9ecb01/pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf", size = 867960 }, + { url = "https://files.pythonhosted.org/packages/ab/68/6fb6ae5551846ad5beca295b7bca32bf0a7ce19f135cb30e55fa2314e6b6/pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e", size = 869204 }, + { url = "https://files.pythonhosted.org/packages/0f/f9/18417771dee223ccf0f48e29adf8b4e25ba6d0e8285e33bcbce078070bc3/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37", size = 1203351 }, + { url = "https://files.pythonhosted.org/packages/e0/46/f13e67fe0d4f8a2315782cbad50493de6203ea0d744610faf4d5f5b16e90/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3", size = 1514204 }, + { url = "https://files.pythonhosted.org/packages/50/11/ddcf7343b7b7a226e0fc7b68cbf5a5bb56291fac07f5c3023bb4c319ebb4/pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6", size = 1414339 }, + { url = "https://files.pythonhosted.org/packages/01/14/1c18d7d5b7be2708f513f37c61bfadfa62161c10624f8733f1c8451b3509/pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4", size = 576928 }, + { url = "https://files.pythonhosted.org/packages/3b/1b/0a540edd75a41df14ec416a9a500b9fec66e554aac920d4c58fbd5756776/pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5", size = 642317 }, + { url = "https://files.pythonhosted.org/packages/98/77/1cbfec0358078a4c5add529d8a70892db1be900980cdb5dd0898b3d6ab9d/pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003", size = 543834 }, + { url = "https://files.pythonhosted.org/packages/28/2f/78a766c8913ad62b28581777ac4ede50c6d9f249d39c2963e279524a1bbe/pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9", size = 1343105 }, + { url = "https://files.pythonhosted.org/packages/b7/9c/4b1e2d3d4065be715e007fe063ec7885978fad285f87eae1436e6c3201f4/pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52", size = 1008365 }, + { url = "https://files.pythonhosted.org/packages/4f/ef/5a23ec689ff36d7625b38d121ef15abfc3631a9aecb417baf7a4245e4124/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08", size = 665923 }, + { url = "https://files.pythonhosted.org/packages/ae/61/d436461a47437d63c6302c90724cf0981883ec57ceb6073873f32172d676/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5", size = 903400 }, + { url = "https://files.pythonhosted.org/packages/47/42/fc6d35ecefe1739a819afaf6f8e686f7f02a4dd241c78972d316f403474c/pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae", size = 860034 }, + { url = "https://files.pythonhosted.org/packages/07/3b/44ea6266a6761e9eefaa37d98fabefa112328808ac41aa87b4bbb668af30/pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711", size = 860579 }, + { url = "https://files.pythonhosted.org/packages/38/6f/4df2014ab553a6052b0e551b37da55166991510f9e1002c89cab7ce3b3f2/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6", size = 1196246 }, + { url = "https://files.pythonhosted.org/packages/38/9d/ee240fc0c9fe9817f0c9127a43238a3e28048795483c403cc10720ddef22/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3", size = 1507441 }, + { url = "https://files.pythonhosted.org/packages/85/4f/01711edaa58d535eac4a26c294c617c9a01f09857c0ce191fd574d06f359/pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b", size = 1406498 }, + { url = "https://files.pythonhosted.org/packages/07/18/907134c85c7152f679ed744e73e645b365f3ad571f38bdb62e36f347699a/pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7", size = 575533 }, + { url = "https://files.pythonhosted.org/packages/ce/2c/a6f4a20202a4d3c582ad93f95ee78d79bbdc26803495aec2912b17dbbb6c/pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a", size = 637768 }, + { url = "https://files.pythonhosted.org/packages/5f/0e/eb16ff731632d30554bf5af4dbba3ffcd04518219d82028aea4ae1b02ca5/pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b", size = 540675 }, + { url = "https://files.pythonhosted.org/packages/04/a7/0f7e2f6c126fe6e62dbae0bc93b1bd3f1099cf7fea47a5468defebe3f39d/pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726", size = 1006564 }, + { url = "https://files.pythonhosted.org/packages/31/b6/a187165c852c5d49f826a690857684333a6a4a065af0a6015572d2284f6a/pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3", size = 1340447 }, + { url = "https://files.pythonhosted.org/packages/68/ba/f4280c58ff71f321602a6e24fd19879b7e79793fb8ab14027027c0fb58ef/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50", size = 665485 }, + { url = "https://files.pythonhosted.org/packages/77/b5/c987a5c53c7d8704216f29fc3d810b32f156bcea488a940e330e1bcbb88d/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb", size = 903484 }, + { url = "https://files.pythonhosted.org/packages/29/c9/07da157d2db18c72a7eccef8e684cefc155b712a88e3d479d930aa9eceba/pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187", size = 859981 }, + { url = "https://files.pythonhosted.org/packages/43/09/e12501bd0b8394b7d02c41efd35c537a1988da67fc9c745cae9c6c776d31/pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b", size = 860334 }, + { url = "https://files.pythonhosted.org/packages/eb/ff/f5ec1d455f8f7385cc0a8b2acd8c807d7fade875c14c44b85c1bddabae21/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18", size = 1196179 }, + { url = "https://files.pythonhosted.org/packages/ec/8a/bb2ac43295b1950fe436a81fc5b298be0b96ac76fb029b514d3ed58f7b27/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115", size = 1507668 }, + { url = "https://files.pythonhosted.org/packages/a9/49/dbc284ebcfd2dca23f6349227ff1616a7ee2c4a35fe0a5d6c3deff2b4fed/pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e", size = 1406539 }, + { url = "https://files.pythonhosted.org/packages/00/68/093cdce3fe31e30a341d8e52a1ad86392e13c57970d722c1f62a1d1a54b6/pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5", size = 575567 }, + { url = "https://files.pythonhosted.org/packages/92/ae/6cc4657148143412b5819b05e362ae7dd09fb9fe76e2a539dcff3d0386bc/pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad", size = 637551 }, + { url = "https://files.pythonhosted.org/packages/6c/67/fbff102e201688f97c8092e4c3445d1c1068c2f27bbd45a578df97ed5f94/pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797", size = 540378 }, + { url = "https://files.pythonhosted.org/packages/3f/fe/2d998380b6e0122c6c4bdf9b6caf490831e5f5e2d08a203b5adff060c226/pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a", size = 1007378 }, + { url = "https://files.pythonhosted.org/packages/4a/f4/30d6e7157f12b3a0390bde94d6a8567cdb88846ed068a6e17238a4ccf600/pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc", size = 1329532 }, + { url = "https://files.pythonhosted.org/packages/82/86/3fe917870e15ee1c3ad48229a2a64458e36036e64b4afa9659045d82bfa8/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5", size = 653242 }, + { url = "https://files.pythonhosted.org/packages/50/2d/242e7e6ef6c8c19e6cb52d095834508cd581ffb925699fd3c640cdc758f1/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672", size = 888404 }, + { url = "https://files.pythonhosted.org/packages/ac/11/7270566e1f31e4ea73c81ec821a4b1688fd551009a3d2bab11ec66cb1e8f/pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797", size = 845858 }, + { url = "https://files.pythonhosted.org/packages/91/d5/72b38fbc69867795c8711bdd735312f9fef1e3d9204e2f63ab57085434b9/pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386", size = 847375 }, + { url = "https://files.pythonhosted.org/packages/dd/9a/10ed3c7f72b4c24e719c59359fbadd1a27556a28b36cdf1cd9e4fb7845d5/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306", size = 1183489 }, + { url = "https://files.pythonhosted.org/packages/72/2d/8660892543fabf1fe41861efa222455811adac9f3c0818d6c3170a1153e3/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6", size = 1492932 }, + { url = "https://files.pythonhosted.org/packages/7b/d6/32fd69744afb53995619bc5effa2a405ae0d343cd3e747d0fbc43fe894ee/pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0", size = 1392485 }, + { url = "https://files.pythonhosted.org/packages/53/fb/36b2b2548286e9444e52fcd198760af99fd89102b5be50f0660fcfe902df/pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072", size = 906955 }, + { url = "https://files.pythonhosted.org/packages/77/8f/6ce54f8979a01656e894946db6299e2273fcee21c8e5fa57c6295ef11f57/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1", size = 565701 }, + { url = "https://files.pythonhosted.org/packages/ee/1c/bf8cd66730a866b16db8483286078892b7f6536f8c389fb46e4beba0a970/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d", size = 794312 }, + { url = "https://files.pythonhosted.org/packages/71/43/91fa4ff25bbfdc914ab6bafa0f03241d69370ef31a761d16bb859f346582/pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca", size = 752775 }, + { url = "https://files.pythonhosted.org/packages/ec/d2/3b2ab40f455a256cb6672186bea95cd97b459ce4594050132d71e76f0d6f/pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c", size = 550762 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "ruff" +version = "0.6.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/f9/4ce3e765a72ab8fe0f80f48508ea38b4196daab3da14d803c21349b2d367/ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18", size = 3084543 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/07/42ee57e8b76ca585297a663a552b4f6d6a99372ca47fdc2276ef72cc0f2f/ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2", size = 10404327 }, + { url = "https://files.pythonhosted.org/packages/eb/51/d42571ff8156d65086acb72d39aa64cb24181db53b497d0ed6293f43f07a/ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c", size = 10018797 }, + { url = "https://files.pythonhosted.org/packages/c1/d7/fa5514a60b03976af972b67fe345deb0335dc96b9f9a9fa4df9890472427/ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5", size = 9691303 }, + { url = "https://files.pythonhosted.org/packages/d6/c4/d812a74976927e51d0782a47539069657ac78535779bfa4d061c4fc8d89d/ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f", size = 10719452 }, + { url = "https://files.pythonhosted.org/packages/ec/b6/aa700c4ae6db9b3ee660e23f3c7db596e2b16a3034b797704fba33ddbc96/ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb", size = 10161353 }, + { url = "https://files.pythonhosted.org/packages/ea/39/0b10075ffcd52ff3a581b9b69eac53579deb230aad300ce8f9d0b58e77bc/ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f", size = 10980630 }, + { url = "https://files.pythonhosted.org/packages/c1/af/9eb9efc98334f62652e2f9318f137b2667187851911fac3b395365a83708/ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0", size = 11768996 }, + { url = "https://files.pythonhosted.org/packages/e0/59/8b1369cf7878358952b1c0a1559b4d6b5c824c003d09b0db26d26c9d094f/ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87", size = 11317469 }, + { url = "https://files.pythonhosted.org/packages/b9/6d/e252e9b11bbca4114c386ee41ad559d0dac13246201d77ea1223c6fea17f/ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098", size = 12467185 }, + { url = "https://files.pythonhosted.org/packages/48/44/7caa223af7d4ea0f0b2bd34acca65a7694a58317714675a2478815ab3f45/ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0", size = 10887766 }, + { url = "https://files.pythonhosted.org/packages/81/ed/394aff3a785f171869158b9d5be61eec9ffb823c3ad5d2bdf2e5f13cb029/ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750", size = 10711609 }, + { url = "https://files.pythonhosted.org/packages/47/31/f31d04c842e54699eab7e3b864538fea26e6c94b71806cd10aa49f13e1c1/ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce", size = 10237621 }, + { url = "https://files.pythonhosted.org/packages/20/95/a764e84acf11d425f2f23b8b78b4fd715e9c20be4aac157c6414ca859a67/ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa", size = 10558329 }, + { url = "https://files.pythonhosted.org/packages/2a/76/d4e38846ac9f6dd62dce858a54583911361b5339dcf8f84419241efac93a/ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44", size = 10954102 }, + { url = "https://files.pythonhosted.org/packages/e7/36/f18c678da6c69f8d022480f3e8ddce6e4a52e07602c1d212056fbd234f8f/ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a", size = 8511090 }, + { url = "https://files.pythonhosted.org/packages/4c/c4/0ca7d8ffa358b109db7d7d045a1a076fd8e5d9cbeae022242d3c060931da/ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263", size = 9350079 }, + { url = "https://files.pythonhosted.org/packages/d9/bd/a8b0c64945a92eaeeb8d0283f27a726a776a1c9d12734d990c5fc7a1278c/ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc", size = 8669595 }, +] + +[[package]] +name = "scipy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/11/4d44a1f274e002784e4dbdb81e0ea96d2de2d1045b2132d5af62cc31fd28/scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417", size = 58620554 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/68/3bc0cfaf64ff507d82b1e5d5b64521df4c8bf7e22bc0b897827cbee9872c/scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389", size = 39069598 }, + { url = "https://files.pythonhosted.org/packages/43/a5/8d02f9c372790326ad405d94f04d4339482ec082455b9e6e288f7100513b/scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3", size = 29879676 }, + { url = "https://files.pythonhosted.org/packages/07/42/0e0bea9666fcbf2cb6ea0205db42c81b1f34d7b729ba251010edf9c80ebd/scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0", size = 23088696 }, + { url = "https://files.pythonhosted.org/packages/15/47/298ab6fef5ebf31b426560e978b8b8548421d4ed0bf99263e1eb44532306/scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3", size = 25470699 }, + { url = "https://files.pythonhosted.org/packages/d8/df/cdb6be5274bc694c4c22862ac3438cb04f360ed9df0aecee02ce0b798380/scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d", size = 35606631 }, + { url = "https://files.pythonhosted.org/packages/47/78/b0c2c23880dd1e99e938ad49ccfb011ae353758a2dc5ed7ee59baff684c3/scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69", size = 41178528 }, + { url = "https://files.pythonhosted.org/packages/5d/aa/994b45c34b897637b853ec04334afa55a85650a0d11dacfa67232260fb0a/scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad", size = 42784535 }, + { url = "https://files.pythonhosted.org/packages/e7/1c/8daa6df17a945cb1a2a1e3bae3c49643f7b3b94017ff01a4787064f03f84/scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5", size = 44772117 }, + { url = "https://files.pythonhosted.org/packages/b2/ab/070ccfabe870d9f105b04aee1e2860520460ef7ca0213172abfe871463b9/scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675", size = 39076999 }, + { url = "https://files.pythonhosted.org/packages/a7/c5/02ac82f9bb8f70818099df7e86c3ad28dae64e1347b421d8e3adf26acab6/scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2", size = 29894570 }, + { url = "https://files.pythonhosted.org/packages/ed/05/7f03e680cc5249c4f96c9e4e845acde08eb1aee5bc216eff8a089baa4ddb/scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617", size = 23103567 }, + { url = "https://files.pythonhosted.org/packages/5e/fc/9f1413bef53171f379d786aabc104d4abeea48ee84c553a3e3d8c9f96a9c/scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8", size = 25499102 }, + { url = "https://files.pythonhosted.org/packages/c2/4b/b44bee3c2ddc316b0159b3d87a3d467ef8d7edfd525e6f7364a62cd87d90/scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37", size = 35586346 }, + { url = "https://files.pythonhosted.org/packages/93/6b/701776d4bd6bdd9b629c387b5140f006185bd8ddea16788a44434376b98f/scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2", size = 41165244 }, + { url = "https://files.pythonhosted.org/packages/06/57/e6aa6f55729a8f245d8a6984f2855696c5992113a5dc789065020f8be753/scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2", size = 42817917 }, + { url = "https://files.pythonhosted.org/packages/ea/c2/5ecadc5fcccefaece775feadcd795060adf5c3b29a883bff0e678cfe89af/scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94", size = 44781033 }, + { url = "https://files.pythonhosted.org/packages/c0/04/2bdacc8ac6387b15db6faa40295f8bd25eccf33f1f13e68a72dc3c60a99e/scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d", size = 39128781 }, + { url = "https://files.pythonhosted.org/packages/c8/53/35b4d41f5fd42f5781dbd0dd6c05d35ba8aa75c84ecddc7d44756cd8da2e/scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07", size = 29939542 }, + { url = "https://files.pythonhosted.org/packages/66/67/6ef192e0e4d77b20cc33a01e743b00bc9e68fb83b88e06e636d2619a8767/scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5", size = 23148375 }, + { url = "https://files.pythonhosted.org/packages/f6/32/3a6dedd51d68eb7b8e7dc7947d5d841bcb699f1bf4463639554986f4d782/scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc", size = 25578573 }, + { url = "https://files.pythonhosted.org/packages/f0/5a/efa92a58dc3a2898705f1dc9dbaf390ca7d4fba26d6ab8cfffb0c72f656f/scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310", size = 35319299 }, + { url = "https://files.pythonhosted.org/packages/8e/ee/8a26858ca517e9c64f84b4c7734b89bda8e63bec85c3d2f432d225bb1886/scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066", size = 40849331 }, + { url = "https://files.pythonhosted.org/packages/a5/cd/06f72bc9187840f1c99e1a8750aad4216fc7dfdd7df46e6280add14b4822/scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1", size = 42544049 }, + { url = "https://files.pythonhosted.org/packages/aa/7d/43ab67228ef98c6b5dd42ab386eae2d7877036970a0d7e3dd3eb47a0d530/scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f", size = 44521212 }, + { url = "https://files.pythonhosted.org/packages/50/ef/ac98346db016ff18a6ad7626a35808f37074d25796fd0234c2bb0ed1e054/scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79", size = 39091068 }, + { url = "https://files.pythonhosted.org/packages/b9/cc/70948fe9f393b911b4251e96b55bbdeaa8cca41f37c26fd1df0232933b9e/scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e", size = 29875417 }, + { url = "https://files.pythonhosted.org/packages/3b/2e/35f549b7d231c1c9f9639f9ef49b815d816bf54dd050da5da1c11517a218/scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73", size = 23084508 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/b028e3f3e59fae61fb8c0f450db732c43dd1d836223a589a8be9f6377203/scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e", size = 25503364 }, + { url = "https://files.pythonhosted.org/packages/a7/2f/6c142b352ac15967744d62b165537a965e95d557085db4beab2a11f7943b/scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d", size = 35292639 }, + { url = "https://files.pythonhosted.org/packages/56/46/2449e6e51e0d7c3575f289f6acb7f828938eaab8874dbccfeb0cd2b71a27/scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e", size = 40798288 }, + { url = "https://files.pythonhosted.org/packages/32/cd/9d86f7ed7f4497c9fd3e39f8918dd93d9f647ba80d7e34e4946c0c2d1a7c/scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06", size = 42524647 }, + { url = "https://files.pythonhosted.org/packages/f5/1b/6ee032251bf4cdb0cc50059374e86a9f076308c1512b61c4e003e241efb7/scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84", size = 44469524 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, +] + +[[package]] +name = "soupsieve" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, +] + +[[package]] +name = "sphinx" +version = "8.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/a7/3cc3d6dcad70aba2e32a3ae8de5a90026a0a2fdaaa0756925e3a120249b6/sphinx-8.0.2.tar.gz", hash = "sha256:0cce1ddcc4fd3532cf1dd283bc7d886758362c5c1de6598696579ce96d8ffa5b", size = 8189041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/61/2ad169c6ff1226b46e50da0e44671592dbc6d840a52034a0193a99b28579/sphinx-8.0.2-py3-none-any.whl", hash = "sha256:56173572ae6c1b9a38911786e206a110c9749116745873feae4f9ce88e59391d", size = 3498950 }, +] + +[[package]] +name = "sphinx-autobuild" +version = "2024.9.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "sphinx" }, + { name = "starlette" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/5b/2adf7fb2451ba9a098c00d40fad0c6ca2cfa967ce0b6c6fa4c36cbc7c70c/sphinx_autobuild-2024.9.19.tar.gz", hash = "sha256:2dd4863d174e533c1cd075eb5dfc90ad9a21734af7efd25569bf228b405e08ef", size = 13712 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/7f/fa4387bbe792bc4592f7e793bc56f7822746d5f26bef0cfc88a262884ec3/sphinx_autobuild-2024.9.19-py3-none-any.whl", hash = "sha256:57d974eebfc6461ff0fd136e78bf7a9c057d543d5166d318a45599898019b82c", size = 11519 }, +] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/ae/a7053b6efb12cf98ccdc59d23672d2da8e73b74bd501f93c68dfecf9535a/sphinx_autodoc_typehints-2.4.4.tar.gz", hash = "sha256:e743512da58b67a06579a1462798a6907664ab77460758a43234adeac350afbf", size = 40572 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/8b/1f0474e576ea6c035ddf7b064474f7d3598edc8404a088ec1194d8386db6/sphinx_autodoc_typehints-2.4.4-py3-none-any.whl", hash = "sha256:940de2951fd584d147e46772579fdc904f945c5f1ee1a78c614646abfbbef18b", size = 19976 }, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496 }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343 }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/e5/0d55470572e0a0934c600c4cda0c98209883aaeb45ff6bfbadcda7006255/sphinx_rtd_theme-0.5.1.tar.gz", hash = "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5", size = 2774928 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/81/d5af3a50a45ee4311ac2dac5b599d69f68388401c7a4ca902e0e450a9f94/sphinx_rtd_theme-0.5.1-py2.py3-none-any.whl", hash = "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113", size = 2793140 }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, +] + +[[package]] +name = "starlette" +version = "0.39.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/0a/62fbd5697f6174041f9b4e2e377b6f383f9189b77dbb7d73d24624caca1d/starlette-0.39.2.tar.gz", hash = "sha256:caaa3b87ef8518ef913dac4f073dea44e85f73343ad2bdc17941931835b2a26a", size = 2573080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/f0/04547f776c8845be46df4bdd1f11159c088bd39e916f35d7da1b9f6eb3ef/starlette-0.39.2-py3-none-any.whl", hash = "sha256:134dd6deb655a9775991d352312d53f1879775e5cc8a481f966e83416a2c3f71", size = 73219 }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "uvicorn" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/96/ee52d900f8e41cc35eaebfda76f3619c2e45b741f3ee957d6fe32be1b2aa/uvicorn-0.31.0.tar.gz", hash = "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906", size = 77140 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/12/206aca5442524d16be7702d08b453d7c274c86fd759266b1f709d4ef43ba/uvicorn-0.31.0-py3-none-any.whl", hash = "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced", size = 63656 }, +] + +[[package]] +name = "virtualenv" +version = "20.26.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 }, +] + +[[package]] +name = "watchfiles" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/a1/631c12626378b9f1538664aa221feb5c60dfafbd7f60b451f8d0bdbcdedd/watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0", size = 375096 }, + { url = "https://files.pythonhosted.org/packages/f7/5c/f27c979c8a10aaa2822286c1bffdce3db731cd1aa4224b9f86623e94bbfe/watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c", size = 367425 }, + { url = "https://files.pythonhosted.org/packages/74/0d/1889e5649885484d29f6c792ef274454d0a26b20d6ed5fdba5409335ccb6/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361", size = 437705 }, + { url = "https://files.pythonhosted.org/packages/85/8a/01d9a22e839f0d1d547af11b1fcac6ba6f889513f1b2e6f221d9d60d9585/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3", size = 433636 }, + { url = "https://files.pythonhosted.org/packages/62/32/a93db78d340c7ef86cde469deb20e36c6b2a873edee81f610e94bbba4e06/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571", size = 451069 }, + { url = "https://files.pythonhosted.org/packages/99/c2/e9e2754fae3c2721c9a7736f92dab73723f1968ed72535fff29e70776008/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd", size = 469306 }, + { url = "https://files.pythonhosted.org/packages/4c/45/f317d9e3affb06c3c27c478de99f7110143e87f0f001f0f72e18d0e1ddce/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a", size = 476187 }, + { url = "https://files.pythonhosted.org/packages/ac/d3/f1f37248abe0114916921e638f71c7d21fe77e3f2f61750e8057d0b68ef2/watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e", size = 425743 }, + { url = "https://files.pythonhosted.org/packages/2b/e8/c7037ea38d838fd81a59cd25761f106ee3ef2cfd3261787bee0c68908171/watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c", size = 612327 }, + { url = "https://files.pythonhosted.org/packages/a0/c5/0e6e228aafe01a7995fbfd2a4edb221bb11a2744803b65a5663fb85e5063/watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188", size = 595096 }, + { url = "https://files.pythonhosted.org/packages/63/d5/4780e8bf3de3b4b46e7428a29654f7dc041cad6b19fd86d083e4b6f64bbe/watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735", size = 264149 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/5148898ba55fc9c111a2a4a5fb67ad3fa7eb2b3d7f0618241ed88749313d/watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04", size = 277542 }, + { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579 }, + { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726 }, + { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735 }, + { url = "https://files.pythonhosted.org/packages/3a/21/0b20bef581a9fbfef290a822c8be645432ceb05fb0741bf3c032e0d90d9a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", size = 433644 }, + { url = "https://files.pythonhosted.org/packages/1c/e8/d5e5f71cc443c85a72e70b24269a30e529227986096abe091040d6358ea9/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", size = 450928 }, + { url = "https://files.pythonhosted.org/packages/61/ee/bf17f5a370c2fcff49e1fec987a6a43fd798d8427ea754ce45b38f9e117a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", size = 469072 }, + { url = "https://files.pythonhosted.org/packages/a3/34/03b66d425986de3fc6077e74a74c78da298f8cb598887f664a4485e55543/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", size = 475517 }, + { url = "https://files.pythonhosted.org/packages/70/eb/82f089c4f44b3171ad87a1b433abb4696f18eb67292909630d886e073abe/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", size = 425480 }, + { url = "https://files.pythonhosted.org/packages/53/20/20509c8f5291e14e8a13104b1808cd7cf5c44acd5feaecb427a49d387774/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", size = 612322 }, + { url = "https://files.pythonhosted.org/packages/df/2b/5f65014a8cecc0a120f5587722068a975a692cadbe9fe4ea56b3d8e43f14/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", size = 595094 }, + { url = "https://files.pythonhosted.org/packages/18/98/006d8043a82c0a09d282d669c88e587b3a05cabdd7f4900e402250a249ac/watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", size = 264191 }, + { url = "https://files.pythonhosted.org/packages/8a/8b/badd9247d6ec25f5f634a9b3d0d92e39c045824ec7e8afcedca8ee52c1e2/watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", size = 277527 }, + { url = "https://files.pythonhosted.org/packages/af/19/35c957c84ee69d904299a38bae3614f7cede45f07f174f6d5a2f4dbd6033/watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", size = 266253 }, + { url = "https://files.pythonhosted.org/packages/35/82/92a7bb6dc82d183e304a5f84ae5437b59ee72d48cee805a9adda2488b237/watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", size = 374137 }, + { url = "https://files.pythonhosted.org/packages/87/91/49e9a497ddaf4da5e3802d51ed67ff33024597c28f652b8ab1e7c0f5718b/watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", size = 367733 }, + { url = "https://files.pythonhosted.org/packages/0d/d8/90eb950ab4998effea2df4cf3a705dc594f6bc501c5a353073aa990be965/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", size = 437322 }, + { url = "https://files.pythonhosted.org/packages/6c/a2/300b22e7bc2a222dd91fce121cefa7b49aa0d26a627b2777e7bdfcf1110b/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", size = 433409 }, + { url = "https://files.pythonhosted.org/packages/99/44/27d7708a43538ed6c26708bcccdde757da8b7efb93f4871d4cc39cffa1cc/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", size = 452142 }, + { url = "https://files.pythonhosted.org/packages/b0/ec/c4e04f755be003129a2c5f3520d2c47026f00da5ecb9ef1e4f9449637571/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", size = 469414 }, + { url = "https://files.pythonhosted.org/packages/c5/4e/cdd7de3e7ac6432b0abf282ec4c1a1a2ec62dfe423cf269b86861667752d/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", size = 472962 }, + { url = "https://files.pythonhosted.org/packages/27/69/e1da9d34da7fc59db358424f5d89a56aaafe09f6961b64e36457a80a7194/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", size = 425705 }, + { url = "https://files.pythonhosted.org/packages/e8/c1/24d0f7357be89be4a43e0a656259676ea3d7a074901f47022f32e2957798/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", size = 612851 }, + { url = "https://files.pythonhosted.org/packages/c7/af/175ba9b268dec56f821639c9893b506c69fd999fe6a2e2c51de420eb2f01/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", size = 594868 }, + { url = "https://files.pythonhosted.org/packages/44/81/1f701323a9f70805bc81c74c990137123344a80ea23ab9504a99492907f8/watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", size = 264109 }, + { url = "https://files.pythonhosted.org/packages/b4/0b/32cde5bc2ebd9f351be326837c61bdeb05ad652b793f25c91cac0b48a60b/watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", size = 277055 }, + { url = "https://files.pythonhosted.org/packages/4b/81/daade76ce33d21dbec7a15afd7479de8db786e5f7b7d249263b4ea174e08/watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", size = 266169 }, + { url = "https://files.pythonhosted.org/packages/30/dc/6e9f5447ae14f645532468a84323a942996d74d5e817837a5c8ce9d16c69/watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", size = 373764 }, + { url = "https://files.pythonhosted.org/packages/79/c0/c3a9929c372816c7fc87d8149bd722608ea58dc0986d3ef7564c79ad7112/watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", size = 367873 }, + { url = "https://files.pythonhosted.org/packages/2e/11/ff9a4445a7cfc1c98caf99042df38964af12eed47d496dd5d0d90417349f/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", size = 438381 }, + { url = "https://files.pythonhosted.org/packages/48/a3/763ba18c98211d7bb6c0f417b2d7946d346cdc359d585cc28a17b48e964b/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", size = 432809 }, + { url = "https://files.pythonhosted.org/packages/30/4c/616c111b9d40eea2547489abaf4ffc84511e86888a166d3a4522c2ba44b5/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", size = 451801 }, + { url = "https://files.pythonhosted.org/packages/b6/be/d7da83307863a422abbfeb12903a76e43200c90ebe5d6afd6a59d158edea/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", size = 468886 }, + { url = "https://files.pythonhosted.org/packages/1d/d3/3dfe131ee59d5e90b932cf56aba5c996309d94dafe3d02d204364c23461c/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", size = 472973 }, + { url = "https://files.pythonhosted.org/packages/42/6c/279288cc5653a289290d183b60a6d80e05f439d5bfdfaf2d113738d0f932/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", size = 425282 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/58afe5e85217e845edf26d8780c2d2d2ae77675eeb8d1b8b8121d799ce52/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", size = 612540 }, + { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625 }, + { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899 }, + { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622 }, + { url = "https://files.pythonhosted.org/packages/df/94/1ad200e937ec91b2a9d6b39ae1cf9c2b1a9cc88d5ceb43aa5c6962eb3c11/watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f", size = 376986 }, + { url = "https://files.pythonhosted.org/packages/ee/fd/d9e020d687ccf90fe95efc513fbb39a8049cf5a3ff51f53c59fcf4c47a5d/watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b", size = 369445 }, + { url = "https://files.pythonhosted.org/packages/43/cb/c0279b35053555d10ef03559c5aebfcb0c703d9c70a7b4e532df74b9b0e8/watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4", size = 439383 }, + { url = "https://files.pythonhosted.org/packages/8b/c4/08b3c2cda45db5169148a981c2100c744a4a222fa7ae7644937c0c002069/watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a", size = 426804 }, +] + +[[package]] +name = "websockets" +version = "13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/73/9223dbc7be3dcaf2a7bbf756c351ec8da04b1fa573edaf545b95f6b0c7fd/websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", size = 158549 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/94/d15dbfc6a5eb636dbc754303fba18208f2e88cf97e733e1d64fb9cb5c89e/websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", size = 157815 }, + { url = "https://files.pythonhosted.org/packages/30/02/c04af33f4663945a26f5e8cf561eb140c35452b50af47a83c3fbcfe62ae1/websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", size = 155466 }, + { url = "https://files.pythonhosted.org/packages/35/e8/719f08d12303ea643655e52d9e9851b2dadbb1991d4926d9ce8862efa2f5/websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6", size = 155716 }, + { url = "https://files.pythonhosted.org/packages/91/e1/14963ae0252a8925f7434065d25dcd4701d5e281a0b4b460a3b5963d2594/websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", size = 164806 }, + { url = "https://files.pythonhosted.org/packages/ec/fa/ab28441bae5e682a0f7ddf3d03440c0c352f930da419301f4a717f675ef3/websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", size = 163810 }, + { url = "https://files.pythonhosted.org/packages/44/77/dea187bd9d16d4b91566a2832be31f99a40d0f5bfa55eeb638eb2c3bc33d/websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", size = 164125 }, + { url = "https://files.pythonhosted.org/packages/cf/d9/3af14544e83f1437eb684b399e6ba0fa769438e869bf5d83d74bc197fae8/websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", size = 164532 }, + { url = "https://files.pythonhosted.org/packages/1c/8a/6d332eabe7d59dfefe4b8ba6f46c8c5fabb15b71c8a8bc3d2b65de19a7b6/websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", size = 163948 }, + { url = "https://files.pythonhosted.org/packages/1a/91/a0aeadbaf3017467a1ee03f8fb67accdae233fe2d5ad4b038c0a84e357b0/websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", size = 163898 }, + { url = "https://files.pythonhosted.org/packages/71/31/a90fb47c63e0ae605be914b0b969d7c6e6ffe2038cd744798e4b3fbce53b/websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", size = 158706 }, + { url = "https://files.pythonhosted.org/packages/93/ca/9540a9ba80da04dc7f36d790c30cae4252589dbd52ccdc92e75b0be22437/websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", size = 159141 }, + { url = "https://files.pythonhosted.org/packages/b2/f0/cf0b8a30d86b49e267ac84addbebbc7a48a6e7bb7c19db80f62411452311/websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", size = 157813 }, + { url = "https://files.pythonhosted.org/packages/bf/e7/22285852502e33071a8cf0ac814f8988480ec6db4754e067b8b9d0e92498/websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", size = 155469 }, + { url = "https://files.pythonhosted.org/packages/68/d4/c8c7c1e5b40ee03c5cc235955b0fb1ec90e7e37685a5f69229ad4708dcde/websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", size = 155717 }, + { url = "https://files.pythonhosted.org/packages/c9/e4/c50999b9b848b1332b07c7fd8886179ac395cb766fda62725d1539e7bc6c/websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", size = 165379 }, + { url = "https://files.pythonhosted.org/packages/bc/49/4a4ad8c072f18fd79ab127650e47b160571aacfc30b110ee305ba25fffc9/websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", size = 164376 }, + { url = "https://files.pythonhosted.org/packages/af/9b/8c06d425a1d5a74fd764dd793edd02be18cf6fc3b1ccd1f29244ba132dc0/websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", size = 164753 }, + { url = "https://files.pythonhosted.org/packages/d5/5b/0acb5815095ff800b579ffc38b13ab1b915b317915023748812d24e0c1ac/websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", size = 165051 }, + { url = "https://files.pythonhosted.org/packages/30/93/c3891c20114eacb1af09dedfcc620c65c397f4fd80a7009cd12d9457f7f5/websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", size = 164489 }, + { url = "https://files.pythonhosted.org/packages/28/09/af9e19885539759efa2e2cd29b8b3f9eecef7ecefea40d46612f12138b36/websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", size = 164438 }, + { url = "https://files.pythonhosted.org/packages/b6/08/6f38b8e625b3d93de731f1d248cc1493327f16cb45b9645b3e791782cff0/websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", size = 158710 }, + { url = "https://files.pythonhosted.org/packages/fb/39/ec8832ecb9bb04a8d318149005ed8cee0ba4e0205835da99e0aa497a091f/websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", size = 159137 }, + { url = "https://files.pythonhosted.org/packages/df/46/c426282f543b3c0296cf964aa5a7bb17e984f58dde23460c3d39b3148fcf/websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", size = 157821 }, + { url = "https://files.pythonhosted.org/packages/aa/85/22529867010baac258da7c45848f9415e6cf37fef00a43856627806ffd04/websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", size = 155480 }, + { url = "https://files.pythonhosted.org/packages/29/2c/bdb339bfbde0119a6e84af43ebf6275278698a2241c2719afc0d8b0bdbf2/websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", size = 155715 }, + { url = "https://files.pythonhosted.org/packages/9f/d0/8612029ea04c5c22bf7af2fd3d63876c4eaeef9b97e86c11972a43aa0e6c/websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", size = 165647 }, + { url = "https://files.pythonhosted.org/packages/56/04/1681ed516fa19ca9083f26d3f3a302257e0911ba75009533ed60fbb7b8d1/websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", size = 164592 }, + { url = "https://files.pythonhosted.org/packages/38/6f/a96417a49c0ed132bb6087e8e39a37db851c70974f5c724a4b2a70066996/websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", size = 165012 }, + { url = "https://files.pythonhosted.org/packages/40/8b/fccf294919a1b37d190e86042e1a907b8f66cff2b61e9befdbce03783e25/websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", size = 165311 }, + { url = "https://files.pythonhosted.org/packages/c1/61/f8615cf7ce5fe538476ab6b4defff52beb7262ff8a73d5ef386322d9761d/websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", size = 164692 }, + { url = "https://files.pythonhosted.org/packages/5c/f1/a29dd6046d3a722d26f182b783a7997d25298873a14028c4760347974ea3/websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", size = 164686 }, + { url = "https://files.pythonhosted.org/packages/0f/99/ab1cdb282f7e595391226f03f9b498f52109d25a2ba03832e21614967dfa/websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", size = 158712 }, + { url = "https://files.pythonhosted.org/packages/46/93/e19160db48b5581feac8468330aa11b7292880a94a37d7030478596cc14e/websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", size = 159145 }, + { url = "https://files.pythonhosted.org/packages/51/20/2b99ca918e1cbd33c53db2cace5f0c0cd8296fc77558e1908799c712e1cd/websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", size = 157828 }, + { url = "https://files.pythonhosted.org/packages/b8/47/0932a71d3d9c0e9483174f60713c84cee58d62839a143f21a2bcdbd2d205/websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", size = 155487 }, + { url = "https://files.pythonhosted.org/packages/a9/60/f1711eb59ac7a6c5e98e5637fef5302f45b6f76a2c9d64fd83bbb341377a/websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", size = 155721 }, + { url = "https://files.pythonhosted.org/packages/6a/e6/ba9a8db7f9d9b0e5f829cf626ff32677f39824968317223605a6b419d445/websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", size = 165609 }, + { url = "https://files.pythonhosted.org/packages/c1/22/4ec80f1b9c27a0aebd84ccd857252eda8418ab9681eb571b37ca4c5e1305/websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", size = 164556 }, + { url = "https://files.pythonhosted.org/packages/27/ac/35f423cb6bb15600438db80755609d27eda36d4c0b3c9d745ea12766c45e/websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", size = 164993 }, + { url = "https://files.pythonhosted.org/packages/31/4e/98db4fd267f8be9e52e86b6ee4e9aa7c42b83452ea0ea0672f176224b977/websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", size = 165360 }, + { url = "https://files.pythonhosted.org/packages/3f/15/3f0de7cda70ffc94b7e7024544072bc5b26e2c1eb36545291abb755d8cdb/websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", size = 164745 }, + { url = "https://files.pythonhosted.org/packages/a1/6e/66b6b756aebbd680b934c8bdbb6dcb9ce45aad72cde5f8a7208dbb00dd36/websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", size = 164732 }, + { url = "https://files.pythonhosted.org/packages/35/c6/12e3aab52c11aeb289e3dbbc05929e7a9d90d7a9173958477d3ef4f8ce2d/websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", size = 158709 }, + { url = "https://files.pythonhosted.org/packages/41/d8/63d6194aae711d7263df4498200c690a9c39fb437ede10f3e157a6343e0d/websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", size = 159144 }, + { url = "https://files.pythonhosted.org/packages/2d/75/6da22cb3ad5b8c606963f9a5f9f88656256fecc29d420b4b2bf9e0c7d56f/websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", size = 155499 }, + { url = "https://files.pythonhosted.org/packages/c0/ba/22833d58629088fcb2ccccedfae725ac0bbcd713319629e97125b52ac681/websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", size = 155737 }, + { url = "https://files.pythonhosted.org/packages/95/54/61684fe22bdb831e9e1843d972adadf359cf04ab8613285282baea6a24bb/websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", size = 157095 }, + { url = "https://files.pythonhosted.org/packages/fc/f5/6652fb82440813822022a9301a30afde85e5ff3fb2aebb77f34aabe2b4e8/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", size = 156701 }, + { url = "https://files.pythonhosted.org/packages/67/33/ae82a7b860fa8a08aba68818bdf7ff61f04598aa5ab96df4cd5a3e418ca4/websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", size = 156654 }, + { url = "https://files.pythonhosted.org/packages/63/0b/a1b528d36934f833e20f6da1032b995bf093d55cb416b9f2266f229fb237/websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", size = 159192 }, + { url = "https://files.pythonhosted.org/packages/56/27/96a5cd2626d11c8280656c6c71d8ab50fe006490ef9971ccd154e0c42cd2/websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", size = 152134 }, +] + +[[package]] +name = "xdoctest" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/a5/7f6dfdaf3a221e16ff79281d2a3c3e4b58989c92de8964a317feb1e6cbb5/xdoctest-1.2.0.tar.gz", hash = "sha256:d8cfca6d8991e488d33f756e600d35b9fdf5efd5c3a249d644efcbbbd2ed5863", size = 204804 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/b8/e4722f5e5f592a665cc8e55a334ea721c359f09574e6b987dc551a1e1f4c/xdoctest-1.2.0-py3-none-any.whl", hash = "sha256:0f1ecf5939a687bd1fc8deefbff1743c65419cce26dff908f8b84c93fbe486bc", size = 151194 }, +]