├── .dockerignore ├── .github └── workflows │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── hue2mqtt.example.toml ├── hue2mqtt ├── __init__.py ├── __main__.py ├── app.py ├── config.py ├── discovery.py ├── hue2mqtt.py ├── messages.py ├── mqtt │ ├── __init__.py │ ├── topic.py │ └── wrapper.py ├── py.typed └── schema.py ├── poetry.lock ├── pyproject.toml ├── stubs └── gmqtt │ ├── __init__.pyi │ ├── client.pyi │ └── constants.pyi └── tests ├── data └── configs │ └── valid.toml ├── mqtt ├── test_topic.py └── test_wrapper.py ├── test_config.py └── test_module.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | Dockerfile* 4 | docker-compose* -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: [push] 3 | jobs: 4 | publish-docker-image: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v2 9 | - name: Login to GitHub Container Registry 10 | uses: docker/login-action@v1 11 | with: 12 | registry: ghcr.io 13 | username: ${{ github.actor }} 14 | password: ${{ secrets.GITHUB_TOKEN }} 15 | - name: Build the Docker image 16 | run: | 17 | docker build . --tag ghcr.io/trickeydan/hue2mqtt-python:latest 18 | docker run ghcr.io/trickeydan/hue2mqtt-python:latest 19 | docker push ghcr.io/trickeydan/hue2mqtt-python:latest -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - "*" 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | python: [3.8, 3.9, "3.10", "3.11", "3.12"] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python }} 19 | uses: actions/setup-python@v1 20 | with: 21 | python-version: ${{ matrix.python }} 22 | - name: Install APT dependencies 23 | run: sudo apt install mosquitto 24 | - name: Install Poetry 25 | run: pip install poetry 26 | - name: Use in-project virtualenv 27 | run: poetry config virtualenvs.in-project true 28 | - uses: actions/cache@v2 29 | with: 30 | path: .venv/ 31 | key: ${{ runner.os }}-${{ matrix.python }}-pip-${{ hashFiles('poetry.lock') }}-${{ hashFiles('pyproject.toml') }} 32 | - name: Install dependencies 33 | run: poetry install 34 | - name: Lint 35 | run: make lint 36 | - name: Static type checking 37 | run: make type 38 | - name: Unit tests 39 | run: make test-cov 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | hue2mqtt.toml 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # Created by https://www.gitignore.io/api/pycharm+all 108 | # Edit at https://www.gitignore.io/?templates=pycharm+all 109 | 110 | ### PyCharm+all ### 111 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 112 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 113 | 114 | # User-specific stuff 115 | .idea/**/workspace.xml 116 | .idea/**/tasks.xml 117 | .idea/**/usage.statistics.xml 118 | .idea/**/dictionaries 119 | .idea/**/shelf 120 | 121 | # Generated files 122 | .idea/**/contentModel.xml 123 | 124 | # Sensitive or high-churn files 125 | .idea/**/dataSources/ 126 | .idea/**/dataSources.ids 127 | .idea/**/dataSources.local.xml 128 | .idea/**/sqlDataSources.xml 129 | .idea/**/dynamic.xml 130 | .idea/**/uiDesigner.xml 131 | .idea/**/dbnavigator.xml 132 | 133 | # Gradle 134 | .idea/**/gradle.xml 135 | .idea/**/libraries 136 | 137 | # Gradle and Maven with auto-import 138 | # When using Gradle or Maven with auto-import, you should exclude module files, 139 | # since they will be recreated, and may cause churn. Uncomment if using 140 | # auto-import. 141 | # .idea/modules.xml 142 | # .idea/*.iml 143 | # .idea/modules 144 | 145 | # CMake 146 | cmake-build-*/ 147 | 148 | # Mongo Explorer plugin 149 | .idea/**/mongoSettings.xml 150 | 151 | # File-based project format 152 | *.iws 153 | 154 | # IntelliJ 155 | out/ 156 | 157 | # mpeltonen/sbt-idea plugin 158 | .idea_modules/ 159 | 160 | # JIRA plugin 161 | atlassian-ide-plugin.xml 162 | 163 | # Cursive Clojure plugin 164 | .idea/replstate.xml 165 | 166 | # Crashlytics plugin (for Android Studio and IntelliJ) 167 | com_crashlytics_export_strings.xml 168 | crashlytics.properties 169 | crashlytics-build.properties 170 | fabric.properties 171 | 172 | # Editor-based Rest Client 173 | .idea/httpRequests 174 | 175 | # Android studio 3.1+ serialized cache file 176 | .idea/caches/build_file_checksums.ser 177 | 178 | ### PyCharm+all Patch ### 179 | # Ignores the whole .idea folder and all .iml files 180 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 181 | 182 | .idea/ 183 | 184 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 185 | 186 | *.iml 187 | modules.xml 188 | .idea/misc.xml 189 | *.ipr 190 | 191 | # End of https://www.gitignore.io/api/pycharm+all 192 | 193 | .vscode 194 | .DS_Store 195 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.0-experimental 2 | 3 | FROM python:3.11-slim 4 | 5 | RUN pip install --upgrade pip 6 | 7 | COPY . /hue2mqtt-python 8 | 9 | WORKDIR /hue2mqtt-python 10 | 11 | RUN pip install . 12 | 13 | VOLUME [ "./hue2mqtt.toml" ] 14 | 15 | CMD ["hue2mqtt"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-23 Dan Trickey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean lint type test test-cov 2 | 3 | CMD:=poetry run 4 | PYMODULE:=hue2mqtt 5 | TESTS:=tests 6 | EXTRACODE:= 7 | 8 | all: type test lint 9 | 10 | lint: 11 | $(CMD) ruff $(PYMODULE) $(TESTS) $(EXTRACODE) 12 | $(CMD) ruff format --check $(PYMODULE) $(TESTS) $(EXTRACODE) 13 | 14 | lint-fix: 15 | $(CMD) ruff --fix $(PYMODULE) $(TESTS) $(EXTRACODE) 16 | $(CMD) ruff format $(PYMODULE) $(TESTS) $(EXTRACODE) 17 | 18 | type: 19 | $(CMD) mypy $(PYMODULE) $(TESTS) $(EXTRACODE) 20 | 21 | test: 22 | $(CMD) pytest --cov=$(PYMODULE) $(TESTS) 23 | 24 | test-cov: 25 | $(CMD) pytest --cov=$(PYMODULE) $(TESTS) --cov-report html 26 | 27 | clean: 28 | git clean -Xdf # Delete all files in .gitignore 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hue2MQTT 2 | 3 | Python Hue to MQTT Bridge 4 | 5 | ## What and Why? 6 | 7 | Hue2MQTT lets you control your Hue setup using MQTT and publishes the current state in real-time. 8 | 9 | - Python 3.8+ with type hints and asyncio 10 | - Uses the excellent [aiohue](https://github.com/home-assistant-libs/aiohue) library to communicate with Hue. 11 | - Control your lights using MQTT 12 | - Receive live events (i.e button pushes, motion sensors) in real-time. 13 | - No polling your Hue Bridge for changes 14 | - IPv6 Support 15 | 16 | ## Configuration 17 | 18 | Hue2MQTT is configured using `hue2mqtt.toml`. 19 | 20 | ```toml 21 | # Hue2MQTT Default Config File 22 | 23 | [mqtt] 24 | # use host.docker.internal to connect to mqtt broker installed on docker host 25 | # host = "host.docker.internal" 26 | host = "::1" 27 | port = 1883 28 | enable_tls = false 29 | force_protocol_version_3_1 = true 30 | 31 | enable_auth = false 32 | username = "" 33 | password = "" 34 | 35 | topic_prefix = "hue2mqtt" 36 | 37 | [hue] 38 | ip = "192.0.2.2" # or IPv6: "[2001:db0::1]" 39 | username = "some secret here" 40 | ``` 41 | 42 | If you do not know the username for your bridge, find it using `hue2mqtt --discover`. 43 | 44 | ## Running Hue2MQTT 45 | 46 | Usually, it is as simple as running `hue2mqtt`. 47 | 48 | ``` 49 | Usage: hue2mqtt [OPTIONS] 50 | 51 | Main function for Hue2MQTT. 52 | 53 | Options: 54 | -v, --verbose 55 | -c, --config-file PATH 56 | --discover 57 | --help Show this message and exit. 58 | ``` 59 | 60 | ## Bridge Status 61 | 62 | The status of Hue2MQTT is published to `hue2mqtt/status` as a JSON object: 63 | 64 | ```json 65 | {"online": true, "bridge": {"name": "Philips Hue", "mac_address": "ec:b5:fa:ab:cd:ef", "api_version": "1.45.0"}} 66 | ``` 67 | 68 | If `online` is `false`, then all other information published by the bridge should be assumed to be inaccurate. 69 | 70 | The `bridge` object contains information about the Hue Bridge, if available. 71 | 72 | ## Getting information about Hue 73 | 74 | Information about the state of Hue is published to MQTT as retained messages. Messages are re-published when the state changes. 75 | 76 | ### Lights 77 | 78 | Information about lights is published to `hue2mqtt/light/{{UNIQUEID}}` where `UNIQUEID` is the Zigbee MAC of the light. 79 | 80 | e.g `hue2mqtt/light/00:17:88:01:ab:cd:ef:01-02` 81 | 82 | ```json 83 | {"id": 1, "name": "Lounge Lamp", "uniqueid": "00:17:88:01:ab:cd:ef:01-02", "state": {"on": false, "alert": "none", "bri": 153, "ct": 497, "effect": "none", "hue": 7170, "sat": 225, "xy": [0, 0], "transitiontime": null, "reachable": true, "color_mode": null, "mode": "homeautomation"}, "manufacturername": "Signify Netherlands B.V.", "modelid": "LCT012", "productname": "Hue color candle", "type": "Extended color light", "swversion": "1.50.2_r30933"} 84 | 85 | ``` 86 | 87 | ### Groups 88 | 89 | A group represents a group of lights, referred to as Rooms and Zones in the Hue app. 90 | 91 | Information about lights is published to `hue2mqtt/group/{{GROUPID}}` where `GROUPID` is an integer. 92 | 93 | ```json 94 | hue2mqtt/group/3 {"id": 3, "name": "Lounge", "lights": [24, 21, 20, 3, 5], "sensors": [], "type": "Room", "state": {"all_on": false, "any_on": false}, "group_class": "Living room", "action": {"on": false, "alert": "none", "bri": 153, "ct": 497, "effect": "none", "hue": 7170, "sat": 225, "xy": [0, 0], "transitiontime": null, "reachable": null, "color_mode": null, "mode": null}} 95 | ``` 96 | 97 | ### Sensors 98 | 99 | Sensors represent other objects in the Hue ecosystem, such as switches and motion sensors. There are also a number of "virtual" sensors that the Hue Hub uses to represent calculated values (e.g `daylight`), but these are ignored by Hue2MQTT. 100 | 101 | Information about sensors is published to `hue2mqtt/sensor/{{UNIQUEID}}` where `UNIQUEID` is the Zigbee MAC of the device. 102 | 103 | e.g `hue2mqtt/sensor/00:17:88:01:ab:cd:ef:01-02` 104 | 105 | **Switch** 106 | 107 | ```json 108 | {"id": 10, "name": "Lounge switch", "type": "ZLLSwitch", "modelid": "RWL021", "manufacturername": "Signify Netherlands B.V.", "productname": "Hue dimmer switch", "uniqueid": "00:17:88:01:ab:cd:ef:01-02", "swversion": "6.1.1.28573", "state": {"lastupdated": "2021-07-10T11:37:58", "buttonevent": 4002}, "capabilities": {"certified": true, "primary": true, "inputs": [{"repeatintervals": [800], "events": [{"buttonevent": 1000, "eventtype": "initial_press"}, {"buttonevent": 1001, "eventtype": "repeat"}, {"buttonevent": 1002, "eventtype": "short_release"}, {"buttonevent": 1003, "eventtype": "long_release"}]}, {"repeatintervals": [800], "events": [{"buttonevent": 2000, "eventtype": "initial_press"}, {"buttonevent": 2001, "eventtype": "repeat"}, {"buttonevent": 2002, "eventtype": "short_release"}, {"buttonevent": 2003, "eventtype": "long_release"}]}, {"repeatintervals": [800], "events": [{"buttonevent": 3000, "eventtype": "initial_press"}, {"buttonevent": 3001, "eventtype": "repeat"}, {"buttonevent": 3002, "eventtype": "short_release"}, {"buttonevent": 3003, "eventtype": "long_release"}]}, {"repeatintervals": [800], "events": [{"buttonevent": 4000, "eventtype": "initial_press"}, {"buttonevent": 4001, "eventtype": "repeat"}, {"buttonevent": 4002, "eventtype": "short_release"}, {"buttonevent": 4003, "eventtype": "long_release"}]}]}} 109 | ``` 110 | 111 | **Light Sensor** 112 | 113 | ```json 114 | {"id": 5, "name": "Hue ambient light sensor 1", "type": "ZLLLightLevel", "modelid": "SML001", "manufacturername": "Signify Netherlands B.V.", "productname": "Hue ambient light sensor", "uniqueid": "00:17:88:01:04:b7:b5:20-02-0400", "swversion": "6.1.1.27575", "state": {"lastupdated": "2021-07-10T12:28:17", "dark": true, "daylight": false, "lightlevel": 14606}, "capabilities": {"certified": true, "primary": false}} 115 | ``` 116 | 117 | ## Controlling Hue 118 | 119 | Lights and Groups can be controlled by publishing objects to the `hue2mqtt/light/{{UNIQUEID}}/set` or `hue2mqtt/group/{{GROUPID}}/set` topics. 120 | 121 | The object should be a JSON object containing the state values that you wish to change. 122 | 123 | ```json 124 | {"on": "true"} 125 | ``` 126 | 127 | ## Docker 128 | 129 | Included is a basic Dockerfile and docker-compose example. 130 | 131 | ### Connections to Docker Host 132 | 133 | To establish a MQTT-Connection to the Docker Host (localhost is the docker instance) use host.docker.internal inside hue2mqtt.toml 134 | 135 | ```toml 136 | host = "host.docker.internal" 137 | ``` 138 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | services: 3 | hue2mqtt: 4 | build: 5 | dockerfile: Dockerfile 6 | context: ./ 7 | container_name: hue2mqtt 8 | restart: always 9 | volumes: 10 | - "./hue2mqtt.toml:/hue2mqtt-python/hue2mqtt.toml:ro" 11 | extra_hosts: 12 | - "host.docker.internal:host-gateway" -------------------------------------------------------------------------------- /hue2mqtt.example.toml: -------------------------------------------------------------------------------- 1 | # Hue2MQTT Default Config File 2 | 3 | [mqtt] 4 | # use host.docker.internal to connect to mqtt broker installed on docker host 5 | # host = "host.docker.internal" 6 | host = "::1" 7 | port = 1883 8 | enable_tls = false 9 | force_protocol_version_3_1 = true 10 | 11 | topic_prefix = "hue2mqtt" 12 | 13 | [hue] 14 | ip = "192.0.2.2" 15 | username = "some secret here" 16 | -------------------------------------------------------------------------------- /hue2mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hue2MQTT. 3 | 4 | Autogenerated by cookiecutter-awesome-poetry. 5 | """ 6 | 7 | __all__ = [ 8 | "__version__", 9 | ] 10 | 11 | __version__ = "0.4.1" 12 | -------------------------------------------------------------------------------- /hue2mqtt/__main__.py: -------------------------------------------------------------------------------- 1 | """Module entrypoint.""" 2 | 3 | from .app import app 4 | 5 | if __name__ == "__main__": 6 | app() 7 | -------------------------------------------------------------------------------- /hue2mqtt/app.py: -------------------------------------------------------------------------------- 1 | """Application entrypoint.""" 2 | import asyncio 3 | from typing import Optional 4 | 5 | import click 6 | 7 | from .discovery import discover_bridge 8 | from .hue2mqtt import Hue2MQTT 9 | 10 | loop = asyncio.get_event_loop() 11 | 12 | 13 | @click.command("hue2mqtt") 14 | @click.option("-v", "--verbose", is_flag=True) 15 | @click.option("-c", "--config-file", type=click.Path(exists=True)) 16 | @click.option("--discover", is_flag=True) 17 | def app(*, verbose: bool, config_file: Optional[str], discover: bool) -> None: 18 | """Main function for Hue2MQTT.""" 19 | if discover: 20 | loop.run_until_complete(discover_bridge()) 21 | else: 22 | # Start application 23 | hue2mqtt = Hue2MQTT(verbose, config_file) 24 | loop.run_until_complete(hue2mqtt.run()) 25 | 26 | 27 | if __name__ == "__main__": 28 | app() 29 | -------------------------------------------------------------------------------- /hue2mqtt/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration schema for Astoria. 3 | 4 | Common to all components. 5 | """ 6 | from pathlib import Path 7 | from typing import IO, Optional 8 | 9 | from pydantic import BaseModel, parse_obj_as 10 | 11 | # Backwards compatibility for TOML in stdlib from Python 3.11 12 | try: 13 | import tomllib # type: ignore[import,unused-ignore] 14 | except ModuleNotFoundError: 15 | import tomli as tomllib # type: ignore[import,no-redef,unused-ignore] 16 | 17 | 18 | class HueBridgeInfo(BaseModel): 19 | """MQTT Broker Information.""" 20 | 21 | ip: str 22 | username: str 23 | 24 | class Config: 25 | """Pydantic config.""" 26 | 27 | extra = "forbid" 28 | 29 | 30 | class MQTTBrokerInfo(BaseModel): 31 | """MQTT Broker Information.""" 32 | 33 | host: str 34 | port: int 35 | enable_auth: bool = False 36 | username: str = "" 37 | password: str = "" 38 | enable_tls: bool = False 39 | topic_prefix: str = "hue2mqtt" 40 | force_protocol_version_3_1: bool = False 41 | 42 | class Config: 43 | """Pydantic config.""" 44 | 45 | extra = "forbid" 46 | 47 | 48 | class Hue2MQTTConfig(BaseModel): 49 | """Config schema for Hue2MQTT.""" 50 | 51 | mqtt: MQTTBrokerInfo 52 | hue: HueBridgeInfo 53 | 54 | class Config: 55 | """Pydantic config.""" 56 | 57 | extra = "forbid" 58 | 59 | @classmethod 60 | def _get_config_path(cls, config_str: Optional[str] = None) -> Path: 61 | """Check for a config file or search the filesystem for one.""" 62 | config_search_paths = [ 63 | Path("hue2mqtt.toml"), 64 | Path("/etc/hue2mqtt.toml"), 65 | ] 66 | if config_str is None: 67 | for path in config_search_paths: 68 | if path.exists() and path.is_file(): 69 | return path 70 | else: 71 | path = Path(config_str) 72 | if path.exists() and path.is_file(): 73 | return path 74 | raise FileNotFoundError("Unable to find config file.") 75 | 76 | @classmethod 77 | def load(cls, config_str: Optional[str] = None) -> "Hue2MQTTConfig": 78 | """Load the config.""" 79 | config_path = cls._get_config_path(config_str) 80 | with config_path.open("rb") as fh: 81 | return cls.load_from_file(fh) 82 | 83 | @classmethod 84 | def load_from_file(cls, fh: IO[bytes]) -> "Hue2MQTTConfig": 85 | """Load the config from a file.""" 86 | return parse_obj_as(cls, tomllib.load(fh)) 87 | -------------------------------------------------------------------------------- /hue2mqtt/discovery.py: -------------------------------------------------------------------------------- 1 | """Hue Bridge Discovery and Configuration.""" 2 | 3 | import sys 4 | 5 | import aiohttp 6 | from aiohue.discovery import discover_nupnp 7 | from aiohue.errors import LinkButtonNotPressed 8 | 9 | 10 | async def discover_bridge() -> None: 11 | """Discover Hue Bridge and get username.""" 12 | print("Searching for local bridges.") 13 | 14 | async with aiohttp.ClientSession() as session: 15 | bridges = await discover_nupnp(session) 16 | 17 | if len(bridges) != 1: 18 | print(f"Error: expected to find exactly 1 bridge, found {len(bridges)}") 19 | sys.exit(1) 20 | else: 21 | bridge = bridges[0] 22 | print("Found bridge at", bridge.host) 23 | try: 24 | await bridge.create_user("hue2mqtt") 25 | print("Your username is", bridge.username) 26 | print("Please add these details to hue2mqtt.toml") 27 | except LinkButtonNotPressed: 28 | print( 29 | "Error: Press the link button on the bridge before running discovery", 30 | ) 31 | sys.exit(1) 32 | -------------------------------------------------------------------------------- /hue2mqtt/hue2mqtt.py: -------------------------------------------------------------------------------- 1 | """ 2 | Data Component base class. 3 | 4 | A data component represents the common functionality between 5 | State Managers and Consumers. It handles connecting to the broker 6 | and managing the event loop. 7 | """ 8 | import asyncio 9 | import json 10 | import logging 11 | import signal 12 | import sys 13 | from signal import SIGHUP, SIGINT, SIGTERM 14 | from types import FrameType 15 | from typing import Match, Optional 16 | 17 | import aiohue 18 | from aiohttp.client import ClientSession 19 | from pydantic import ValidationError, parse_obj_as 20 | 21 | from hue2mqtt import __version__ 22 | from hue2mqtt.messages import BridgeInfo, Hue2MQTTStatus 23 | from hue2mqtt.schema import ( 24 | GroupInfo, 25 | GroupSetState, 26 | LightInfo, 27 | LightSetState, 28 | SensorInfo, 29 | ) 30 | 31 | from .config import Hue2MQTTConfig 32 | from .mqtt.wrapper import MQTTWrapper 33 | 34 | LOGGER = logging.getLogger(__name__) 35 | 36 | loop = asyncio.get_event_loop() 37 | 38 | 39 | class Hue2MQTT: 40 | """Hue to MQTT Bridge.""" 41 | 42 | config: Hue2MQTTConfig 43 | 44 | def __init__( 45 | self, 46 | verbose: bool, 47 | config_file: Optional[str], 48 | *, 49 | name: str = "hue2mqtt", 50 | ) -> None: 51 | self.config = Hue2MQTTConfig.load(config_file) 52 | self.name = name 53 | 54 | self._setup_logging(verbose) 55 | self._setup_event_loop() 56 | self._setup_mqtt() 57 | 58 | def _setup_logging(self, verbose: bool, *, welcome_message: bool = True) -> None: 59 | if verbose: 60 | logging.basicConfig( 61 | level=logging.DEBUG, 62 | format=f"%(asctime)s {self.name} %(name)s %(levelname)s %(message)s", 63 | datefmt="%Y-%m-%d %H:%M:%S", 64 | ) 65 | else: 66 | logging.basicConfig( 67 | level=logging.INFO, 68 | format=f"%(asctime)s {self.name} %(levelname)s %(message)s", 69 | datefmt="%Y-%m-%d %H:%M:%S", 70 | ) 71 | 72 | # Suppress INFO messages from gmqtt 73 | logging.getLogger("gmqtt").setLevel(logging.WARNING) 74 | 75 | if welcome_message: 76 | LOGGER.info(f"Hue2MQTT v{__version__} - {self.__doc__}") 77 | 78 | def _setup_event_loop(self) -> None: 79 | loop.add_signal_handler(SIGHUP, self.halt) 80 | loop.add_signal_handler(SIGINT, self.halt) 81 | loop.add_signal_handler(SIGTERM, self.halt) 82 | 83 | def _setup_mqtt(self) -> None: 84 | self._mqtt = MQTTWrapper( 85 | self.name, 86 | self.config.mqtt, 87 | last_will=Hue2MQTTStatus(online=False), 88 | ) 89 | 90 | self._mqtt.subscribe("light/+/set", self.handle_set_light) 91 | self._mqtt.subscribe("group/+/set", self.handle_set_group) 92 | 93 | def _exit(self, signals: signal.Signals, frame_type: FrameType) -> None: 94 | sys.exit(0) 95 | 96 | async def run(self) -> None: 97 | """Entrypoint for the data component.""" 98 | await self._mqtt.connect() 99 | LOGGER.info("Connected to MQTT Broker") 100 | 101 | async with ClientSession() as websession: 102 | try: 103 | await self._setup_bridge(websession) 104 | except aiohue.errors.Unauthorized: 105 | LOGGER.error("Bridge rejected username. Please use --discover") 106 | self.halt() 107 | return 108 | await self._publish_bridge_status() 109 | await self.main(websession) 110 | 111 | LOGGER.info("Disconnecting from MQTT Broker") 112 | await self._publish_bridge_status(online=False) 113 | await self._mqtt.disconnect() 114 | 115 | def halt(self) -> None: 116 | """Stop the component.""" 117 | sys.exit(-1) 118 | 119 | async def _setup_bridge(self, websession: ClientSession) -> None: 120 | """Connect to the Hue Bridge.""" 121 | self._bridge = aiohue.Bridge( 122 | self.config.hue.ip, 123 | websession, 124 | username=self.config.hue.username, 125 | ) 126 | LOGGER.info(f"Connecting to Hue Bridge at {self.config.hue.ip}") 127 | await self._bridge.initialize() 128 | 129 | async def _publish_bridge_status(self, *, online: bool = True) -> None: 130 | """Publish info about the Hue Bridge.""" 131 | if online: 132 | LOGGER.info(f"Bridge Name: {self._bridge.config.name}") 133 | LOGGER.info(f"Bridge MAC: {self._bridge.config.mac}") 134 | LOGGER.info(f"API Version: {self._bridge.config.apiversion}") 135 | 136 | info = BridgeInfo( 137 | name=self._bridge.config.name, 138 | mac_address=self._bridge.config.mac, 139 | api_version=self._bridge.config.apiversion, 140 | ) 141 | message = Hue2MQTTStatus(online=online, bridge=info) 142 | else: 143 | message = Hue2MQTTStatus(online=online) 144 | 145 | self._mqtt.publish("status", message) 146 | 147 | def publish_light(self, light: LightInfo) -> None: 148 | """Publish information about a light to MQTT.""" 149 | self._mqtt.publish(f"light/{light.uniqueid}", light, retain=True) 150 | 151 | def publish_group(self, group: GroupInfo) -> None: 152 | """Publish information about a group to MQTT.""" 153 | self._mqtt.publish(f"group/{group.id}", group, retain=True) 154 | 155 | def publish_sensor(self, sensor: SensorInfo) -> None: 156 | """Publish information about a group to MQTT.""" 157 | self._mqtt.publish(f"sensor/{sensor.uniqueid}", sensor, retain=True) 158 | 159 | async def handle_set_light(self, match: Match[str], payload: str) -> None: 160 | """Handle an update to a light.""" 161 | uniqueid = match.group(1) 162 | 163 | # Find the light with that uniqueid 164 | for light_id in self._bridge.lights: 165 | light = self._bridge.lights[light_id] 166 | if light.uniqueid == uniqueid: 167 | try: 168 | state = parse_obj_as(LightSetState, json.loads(payload)) 169 | LOGGER.info(f"Updating {light.name}") 170 | await light.set_state(**state.dict()) 171 | except json.JSONDecodeError: 172 | LOGGER.warning(f"Bad JSON on light request: {payload}") 173 | except TypeError: 174 | LOGGER.warning(f"Expected dictionary, got: {payload}") 175 | except ValidationError as e: 176 | LOGGER.warning(f"Invalid light state: {e}") 177 | return 178 | LOGGER.warning(f"Unknown light uniqueid: {uniqueid}") 179 | 180 | async def handle_set_group(self, match: Match[str], payload: str) -> None: 181 | """Handle an update to a group.""" 182 | groupid = match.group(1) 183 | 184 | try: 185 | group = self._bridge.groups[groupid] 186 | state = parse_obj_as(GroupSetState, json.loads(payload)) 187 | LOGGER.info(f"Updating group {group.name}") 188 | await group.set_action(**state.dict()) 189 | except IndexError: 190 | LOGGER.warning(f"Unknown group id: {groupid}") 191 | except json.JSONDecodeError: 192 | LOGGER.warning(f"Bad JSON on light request: {payload}") 193 | except TypeError: 194 | LOGGER.warning(f"Expected dictionary, got: {payload}") 195 | except ValidationError as e: 196 | LOGGER.warning(f"Invalid light state: {e}") 197 | 198 | async def main(self, websession: ClientSession) -> None: 199 | """Main method of the data component.""" 200 | # Publish initial info about lights 201 | for idx, light_raw in self._bridge.lights._items.items(): 202 | light = LightInfo(id=idx, **light_raw.raw) 203 | self.publish_light(light) 204 | 205 | # Publish initial info about groups 206 | for idx, group_raw in self._bridge.groups._items.items(): 207 | group = GroupInfo(id=idx, **group_raw.raw) 208 | self.publish_group(group) 209 | 210 | # Publish initial info about sensors 211 | for idx, sensor_raw in self._bridge.sensors._items.items(): 212 | if "uniqueid" in sensor_raw.raw and "productname" in sensor_raw.raw: 213 | sensor = SensorInfo(id=idx, **sensor_raw.raw) 214 | self.publish_sensor(sensor) 215 | else: 216 | LOGGER.debug(f"Ignoring virtual sensor: {sensor_raw.name}") 217 | 218 | # Publish updates 219 | try: 220 | async for updated_object in self._bridge.listen_events(): 221 | if isinstance(updated_object, aiohue.groups.Group): 222 | group = GroupInfo(id=updated_object.id, **updated_object.raw) 223 | self.publish_group(group) 224 | elif isinstance(updated_object, aiohue.lights.Light): 225 | light = LightInfo(id=updated_object.id, **updated_object.raw) 226 | self.publish_light(light) 227 | elif isinstance(updated_object, aiohue.sensors.GenericSensor): 228 | sensor = SensorInfo(id=updated_object.id, **updated_object.raw) 229 | self.publish_sensor(sensor) 230 | else: 231 | LOGGER.warning("Unknown object") 232 | except GeneratorExit: 233 | LOGGER.warning("Exited loop") 234 | -------------------------------------------------------------------------------- /hue2mqtt/messages.py: -------------------------------------------------------------------------------- 1 | """Schemas for MQTT Messages.""" 2 | from typing import Optional 3 | 4 | from pydantic import BaseModel 5 | 6 | 7 | class BridgeInfo(BaseModel): 8 | """Information about the Hue Bridge.""" 9 | 10 | name: str 11 | mac_address: str 12 | api_version: str 13 | 14 | 15 | class Hue2MQTTStatus(BaseModel): 16 | """Status of Hue2MQTT.""" 17 | 18 | online: bool 19 | bridge: Optional[BridgeInfo] = None 20 | -------------------------------------------------------------------------------- /hue2mqtt/mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | """MQTT Helper Functions and Classes.""" 2 | 3 | from .topic import Topic 4 | 5 | __all__ = ["Topic"] 6 | -------------------------------------------------------------------------------- /hue2mqtt/mqtt/topic.py: -------------------------------------------------------------------------------- 1 | """ 2 | MQTT Topic Abstraction. 3 | 4 | Allows topic strings to be constructed, along with regex to match them. 5 | """ 6 | 7 | from re import compile 8 | from typing import Dict, Match, Optional, Pattern, Sequence 9 | 10 | 11 | class Topic: 12 | """ 13 | An MQTT Topic. 14 | 15 | A topic that may be published or subscribed to. 16 | """ 17 | 18 | WILDCARDS: Dict[str, str] = { 19 | "+": "([^/]+)", 20 | "#": "(.+)", 21 | } 22 | 23 | def __init__(self, parts: Sequence[str]) -> None: 24 | self.parts = parts 25 | 26 | def match(self, topic: str) -> Optional[Match[str]]: 27 | """Perform a regex match on a topic.""" 28 | return self.regex.match(topic) 29 | 30 | @classmethod 31 | def parse(cls, topic: str) -> "Topic": 32 | """ 33 | Parse a string topic into a Topic instance. 34 | 35 | It is assumed that the supplied topic complies with the MQTT spec. 36 | If not, then behaviour is undefined. 37 | """ 38 | if len(topic) > 1: 39 | if topic[:1] == "/" or topic[-1:] == "/": 40 | raise ValueError("Topic cannot begin or end with /") 41 | else: 42 | if topic == "/" or len(topic) == 0: 43 | raise ValueError("Invalid Topic") 44 | 45 | return cls(topic.split("/")) 46 | 47 | def __str__(self) -> str: 48 | return "/".join(str(p) for p in self.parts) 49 | 50 | def __repr__(self) -> str: 51 | return f'Topic("{self}")' 52 | 53 | def __hash__(self) -> int: 54 | return hash(repr(self)) 55 | 56 | def __eq__(self, other: object) -> bool: 57 | try: 58 | return self.parts == other.parts # type: ignore 59 | except AttributeError: 60 | return False 61 | 62 | @property 63 | def is_publishable(self) -> bool: 64 | """ 65 | Determine if it is valid to publish to this topic. 66 | 67 | We are not permitting publication to topics containing wildcards. 68 | """ 69 | return all(x not in self.parts for x in self.WILDCARDS) 70 | 71 | @property 72 | def regex(self) -> Pattern[str]: 73 | """ 74 | Regular expression to match the topic. 75 | 76 | Any wildcard fields are available as capture groups. 77 | """ 78 | handled_parts = [] 79 | for p in self.parts: 80 | try: 81 | handled_parts.append(self.WILDCARDS[p]) 82 | except KeyError: 83 | handled_parts.append(p) 84 | 85 | return compile("^" + "/".join(handled_parts) + "$") 86 | -------------------------------------------------------------------------------- /hue2mqtt/mqtt/wrapper.py: -------------------------------------------------------------------------------- 1 | """MQTT Wrapper.""" 2 | 3 | import asyncio 4 | import logging 5 | from typing import Any, Callable, Coroutine, Dict, List, Match, Optional 6 | 7 | import gmqtt 8 | from pydantic import BaseModel 9 | 10 | from hue2mqtt.config import MQTTBrokerInfo 11 | 12 | from .topic import Topic 13 | 14 | LOGGER = logging.getLogger(__name__) 15 | 16 | Handler = Callable[[Match[str], str], Coroutine[Any, Any, None]] 17 | 18 | 19 | class MQTTWrapper: 20 | """ 21 | MQTT wrapper class. 22 | 23 | Wraps the functionality that we are using for MQTT, with extra 24 | sanity checks and validation to make sure that things are less 25 | likely to go wrong. 26 | """ 27 | 28 | _client: gmqtt.Client 29 | 30 | def __init__( 31 | self, 32 | client_name: str, 33 | broker_info: MQTTBrokerInfo, 34 | *, 35 | last_will: Optional[BaseModel] = None, 36 | ) -> None: 37 | self._client_name = client_name 38 | self._broker_info = broker_info 39 | self._last_will = last_will 40 | 41 | self._topic_handlers: Dict[Topic, Handler] = {} 42 | 43 | self._client = gmqtt.Client( 44 | self._client_name, 45 | will_message=self.last_will_message, 46 | ) 47 | 48 | self._client.reconnect_retries = 0 49 | 50 | self._client.on_message = self.on_message 51 | self._client.on_connect = self.on_connect 52 | 53 | @property 54 | def is_connected(self) -> bool: 55 | """Determine if the client connected to the broker.""" 56 | return self._client.is_connected 57 | 58 | @property 59 | def last_will_message(self) -> Optional[gmqtt.Message]: 60 | """Last will and testament message for this client.""" 61 | if self._last_will is not None: 62 | return gmqtt.Message( 63 | self.mqtt_prefix + "/" + "status", 64 | self._last_will.json(), 65 | retain=True, 66 | ) 67 | else: 68 | return None 69 | 70 | @property 71 | def mqtt_prefix(self) -> str: 72 | """The topic prefix for MQTT.""" 73 | return self._broker_info.topic_prefix 74 | 75 | async def connect(self) -> None: 76 | """Connect to the broker.""" 77 | if self.is_connected: 78 | LOGGER.error("Attempting connection, but client is already connected.") 79 | mqtt_version = gmqtt.constants.MQTTv50 80 | if self._broker_info.force_protocol_version_3_1: 81 | mqtt_version = gmqtt.constants.MQTTv311 82 | 83 | if self._broker_info.enable_auth: 84 | LOGGER.debug("MQTT Auth enabled") 85 | self._client.set_auth_credentials( 86 | self._broker_info.username, 87 | self._broker_info.password, 88 | ) 89 | 90 | await self._client.connect( 91 | self._broker_info.host, 92 | port=self._broker_info.port, 93 | ssl=self._broker_info.enable_tls, 94 | version=mqtt_version, 95 | ) 96 | 97 | async def disconnect(self) -> None: 98 | """Disconnect from the broker.""" 99 | if not self.is_connected: 100 | LOGGER.error( 101 | "Attempting disconnection, but client is already disconnected.", 102 | ) 103 | 104 | await self._client.disconnect() 105 | 106 | if self.is_connected: 107 | raise RuntimeError("Disconnection was attempted, but was unsuccessful") 108 | 109 | def on_connect( 110 | self, 111 | client: gmqtt.client.Client, 112 | flags: int, 113 | rc: int, 114 | properties: Dict[str, List[int]], 115 | ) -> None: 116 | """Callback for mqtt connection.""" 117 | for topic in self._topic_handlers: 118 | LOGGER.debug(f"Subscribing to {topic}") 119 | client.subscribe(str(topic)) 120 | 121 | async def on_message( 122 | self, 123 | client: gmqtt.client.Client, 124 | topic: str, 125 | payload: bytes, 126 | qos: int, 127 | properties: Dict[str, int], 128 | ) -> gmqtt.constants.PubRecReasonCode: 129 | """Callback for mqtt messages.""" 130 | LOGGER.debug(f"Message received on {topic} with payload: {payload!r}") 131 | for t, handler in self._topic_handlers.items(): 132 | match = t.match(topic) 133 | if match: 134 | LOGGER.debug(f"Calling {handler.__name__} to handle {topic}") 135 | asyncio.ensure_future(handler(match, payload.decode())) 136 | 137 | return gmqtt.constants.PubRecReasonCode.SUCCESS 138 | 139 | def publish( 140 | self, 141 | topic: str, 142 | payload: BaseModel, 143 | *, 144 | retain: bool = False, 145 | auto_prefix_topic: bool = True, 146 | ) -> None: 147 | """Publish a payload to the broker.""" 148 | if not self.is_connected: 149 | LOGGER.error( 150 | "Attempted to publish message, but client is not connected.", 151 | ) 152 | 153 | prefix = self._broker_info.topic_prefix 154 | 155 | if len(topic) == 0: 156 | topic_complete = Topic.parse(prefix) 157 | elif auto_prefix_topic: 158 | topic_complete = Topic.parse(f"{prefix}/{topic}") 159 | else: 160 | topic_complete = Topic.parse(topic) 161 | 162 | if not topic_complete.is_publishable: 163 | raise ValueError(f"Cannot publish to MQTT topic: {topic_complete}") 164 | 165 | self._client.publish( 166 | str(topic_complete), 167 | payload.json(by_alias=True, exclude_none=True), 168 | qos=1, 169 | retain=retain, 170 | ) 171 | 172 | def subscribe( 173 | self, 174 | topic: str, 175 | callback: Handler, 176 | ) -> None: 177 | """ 178 | Subscribe to an MQTT Topic. 179 | 180 | Callback is called when a message arrives. 181 | 182 | Should be called before the MQTT wrapper is connected. 183 | """ 184 | if len(topic) == 0: 185 | topic_complete = Topic.parse(self.mqtt_prefix) 186 | else: 187 | topic_complete = Topic.parse(f"{self._broker_info.topic_prefix}/{topic}") 188 | 189 | self._topic_handlers[topic_complete] = callback 190 | -------------------------------------------------------------------------------- /hue2mqtt/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. Hue2MQTT uses inline type hints. See PEP 484 for more information. 2 | -------------------------------------------------------------------------------- /hue2mqtt/schema.py: -------------------------------------------------------------------------------- 1 | """Schemas for data about lights.""" 2 | from typing import Any, List, Optional, Tuple 3 | 4 | from pydantic import BaseModel, Field, create_model 5 | 6 | 7 | class LightBaseState(BaseModel): 8 | """The base attributes of a light state.""" 9 | 10 | on: Optional[bool] 11 | 12 | alert: Optional[str] 13 | bri: Optional[int] 14 | ct: Optional[int] 15 | effect: Optional[str] 16 | hue: Optional[int] 17 | sat: Optional[int] 18 | xy: Optional[Tuple[float, float]] 19 | transitiontime: Optional[str] 20 | 21 | 22 | class LightSetState(LightBaseState): 23 | """The settable states of a light.""" 24 | 25 | bri_inc: Optional[int] 26 | sat_inc: Optional[int] 27 | hue_inc: Optional[int] 28 | ct_inc: Optional[int] 29 | xy_inc: Optional[Tuple[float, float]] 30 | 31 | 32 | class GroupSetState(LightSetState): 33 | """The settable states of a group.""" 34 | 35 | scene: Optional[str] 36 | 37 | 38 | class LightState(LightBaseState): 39 | """The State of a light that we can read.""" 40 | 41 | reachable: Optional[bool] 42 | color_mode: Optional[str] 43 | mode: Optional[str] 44 | 45 | 46 | class LightInfo(BaseModel): 47 | """Information about a light.""" 48 | 49 | id: int # noqa: A003 50 | name: str 51 | uniqueid: str 52 | state: Optional[LightState] 53 | 54 | manufacturername: str 55 | modelid: str 56 | productname: str 57 | type: str # noqa: A003 58 | 59 | swversion: str 60 | 61 | 62 | class GroupState(BaseModel): 63 | """The state of lights in a group.""" 64 | 65 | all_on: bool 66 | any_on: bool 67 | 68 | 69 | class GroupInfo(BaseModel): 70 | """Information about a light group.""" 71 | 72 | id: int # noqa: A003 73 | name: str 74 | lights: List[int] 75 | sensors: List[int] 76 | type: str # noqa: A003 77 | state: GroupState 78 | 79 | group_class: Optional[str] = Field(default=None, alias="class") 80 | 81 | action: LightState 82 | 83 | 84 | class GenericSensorState(BaseModel): 85 | """Information about the state of a sensor.""" 86 | 87 | lastupdated: Optional[str] = None 88 | 89 | 90 | class PresenceSensorState(GenericSensorState): 91 | """Information about the state of a sensor.""" 92 | 93 | presence: Optional[bool] = None 94 | 95 | 96 | class RotarySensorState(GenericSensorState): 97 | """Information about the state of a sensor.""" 98 | 99 | rotaryevent: Optional[str] = None 100 | expectedrotation: Optional[str] = None 101 | expectedeventduration: Optional[str] = None 102 | 103 | 104 | class SwitchSensorState(GenericSensorState): 105 | """Information about the state of a sensor.""" 106 | 107 | buttonevent: Optional[int] = None 108 | 109 | 110 | class LightLevelSensorState(GenericSensorState): 111 | """Information about the state of a sensor.""" 112 | 113 | dark: Optional[bool] = None 114 | daylight: Optional[bool] = None 115 | lightlevel: Optional[int] = None 116 | 117 | 118 | class TemperatureSensorState(GenericSensorState): 119 | """Information about the state of a sensor.""" 120 | 121 | temperature: Optional[int] = None 122 | 123 | 124 | class HumiditySensorState(GenericSensorState): 125 | """Information about the state of a sensor.""" 126 | 127 | humidity: Optional[int] = None 128 | 129 | 130 | class OpenCloseSensorState(GenericSensorState): 131 | """Information about the state of a sensor.""" 132 | 133 | open: Optional[str] = None # noqa: A003 134 | 135 | 136 | SensorState = create_model( 137 | "SensorState", 138 | __base__=( 139 | LightLevelSensorState, 140 | PresenceSensorState, 141 | RotarySensorState, 142 | SwitchSensorState, 143 | TemperatureSensorState, 144 | HumiditySensorState, 145 | OpenCloseSensorState, 146 | ), 147 | ) 148 | 149 | 150 | class SensorInfo(BaseModel): 151 | """Information about a sensor.""" 152 | 153 | id: int # noqa: A003 154 | name: str 155 | type: str # noqa: A003 156 | modelid: str 157 | manufacturername: str 158 | 159 | productname: str 160 | uniqueid: str 161 | swversion: Optional[str] 162 | 163 | state: SensorState # type: ignore[valid-type] 164 | capabilities: Any 165 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aiohttp" 5 | version = "3.9.1" 6 | description = "Async http client/server framework (asyncio)" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590"}, 11 | {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0"}, 12 | {file = "aiohttp-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83"}, 13 | {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5"}, 14 | {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4"}, 15 | {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f"}, 16 | {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f"}, 17 | {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6"}, 18 | {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f"}, 19 | {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26"}, 20 | {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4"}, 21 | {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d"}, 22 | {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501"}, 23 | {file = "aiohttp-3.9.1-cp310-cp310-win32.whl", hash = "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489"}, 24 | {file = "aiohttp-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23"}, 25 | {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d"}, 26 | {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e"}, 27 | {file = "aiohttp-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0"}, 28 | {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35"}, 29 | {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff"}, 30 | {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87"}, 31 | {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d"}, 32 | {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01"}, 33 | {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3"}, 34 | {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449"}, 35 | {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2"}, 36 | {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd"}, 37 | {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a"}, 38 | {file = "aiohttp-3.9.1-cp311-cp311-win32.whl", hash = "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544"}, 39 | {file = "aiohttp-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587"}, 40 | {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065"}, 41 | {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821"}, 42 | {file = "aiohttp-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af"}, 43 | {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57"}, 44 | {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5"}, 45 | {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb"}, 46 | {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c"}, 47 | {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66"}, 48 | {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe"}, 49 | {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183"}, 50 | {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b"}, 51 | {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f"}, 52 | {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f"}, 53 | {file = "aiohttp-3.9.1-cp312-cp312-win32.whl", hash = "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed"}, 54 | {file = "aiohttp-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213"}, 55 | {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70"}, 56 | {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672"}, 57 | {file = "aiohttp-3.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1"}, 58 | {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd"}, 59 | {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690"}, 60 | {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca"}, 61 | {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361"}, 62 | {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28"}, 63 | {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014"}, 64 | {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431"}, 65 | {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd"}, 66 | {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a"}, 67 | {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8"}, 68 | {file = "aiohttp-3.9.1-cp38-cp38-win32.whl", hash = "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4"}, 69 | {file = "aiohttp-3.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7"}, 70 | {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766"}, 71 | {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0"}, 72 | {file = "aiohttp-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558"}, 73 | {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636"}, 74 | {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499"}, 75 | {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4"}, 76 | {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f"}, 77 | {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a"}, 78 | {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e"}, 79 | {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5"}, 80 | {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0"}, 81 | {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f"}, 82 | {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c"}, 83 | {file = "aiohttp-3.9.1-cp39-cp39-win32.whl", hash = "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7"}, 84 | {file = "aiohttp-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf"}, 85 | {file = "aiohttp-3.9.1.tar.gz", hash = "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d"}, 86 | ] 87 | 88 | [package.dependencies] 89 | aiosignal = ">=1.1.2" 90 | async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} 91 | attrs = ">=17.3.0" 92 | frozenlist = ">=1.1.1" 93 | multidict = ">=4.5,<7.0" 94 | yarl = ">=1.0,<2.0" 95 | 96 | [package.extras] 97 | speedups = ["Brotli", "aiodns", "brotlicffi"] 98 | 99 | [[package]] 100 | name = "aiohue" 101 | version = "2.6.3" 102 | description = "Python module to talk to Philips Hue." 103 | optional = false 104 | python-versions = "*" 105 | files = [ 106 | {file = "aiohue-2.6.3-py3-none-any.whl", hash = "sha256:78c8d9bf7517a3a8e074a99c0327106da09a0ada75385ef98acc8e2902b721af"}, 107 | {file = "aiohue-2.6.3.tar.gz", hash = "sha256:ce9c240ca3eb1394c56503b403589f4d0ee7f93445a578b78da8b7879a65c863"}, 108 | ] 109 | 110 | [package.dependencies] 111 | aiohttp = "*" 112 | 113 | [[package]] 114 | name = "aiosignal" 115 | version = "1.3.1" 116 | description = "aiosignal: a list of registered asynchronous callbacks" 117 | optional = false 118 | python-versions = ">=3.7" 119 | files = [ 120 | {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, 121 | {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, 122 | ] 123 | 124 | [package.dependencies] 125 | frozenlist = ">=1.1.0" 126 | 127 | [[package]] 128 | name = "async-timeout" 129 | version = "4.0.3" 130 | description = "Timeout context manager for asyncio programs" 131 | optional = false 132 | python-versions = ">=3.7" 133 | files = [ 134 | {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, 135 | {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, 136 | ] 137 | 138 | [[package]] 139 | name = "attrs" 140 | version = "23.1.0" 141 | description = "Classes Without Boilerplate" 142 | optional = false 143 | python-versions = ">=3.7" 144 | files = [ 145 | {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, 146 | {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, 147 | ] 148 | 149 | [package.extras] 150 | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] 151 | dev = ["attrs[docs,tests]", "pre-commit"] 152 | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] 153 | tests = ["attrs[tests-no-zope]", "zope-interface"] 154 | tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] 155 | 156 | [[package]] 157 | name = "click" 158 | version = "8.1.7" 159 | description = "Composable command line interface toolkit" 160 | optional = false 161 | python-versions = ">=3.7" 162 | files = [ 163 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 164 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 165 | ] 166 | 167 | [package.dependencies] 168 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 169 | 170 | [[package]] 171 | name = "colorama" 172 | version = "0.4.6" 173 | description = "Cross-platform colored terminal text." 174 | optional = false 175 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 176 | files = [ 177 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 178 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 179 | ] 180 | 181 | [[package]] 182 | name = "coverage" 183 | version = "7.3.3" 184 | description = "Code coverage measurement for Python" 185 | optional = false 186 | python-versions = ">=3.8" 187 | files = [ 188 | {file = "coverage-7.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d874434e0cb7b90f7af2b6e3309b0733cde8ec1476eb47db148ed7deeb2a9494"}, 189 | {file = "coverage-7.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee6621dccce8af666b8c4651f9f43467bfbf409607c604b840b78f4ff3619aeb"}, 190 | {file = "coverage-7.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1367aa411afb4431ab58fd7ee102adb2665894d047c490649e86219327183134"}, 191 | {file = "coverage-7.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f0f8f0c497eb9c9f18f21de0750c8d8b4b9c7000b43996a094290b59d0e7523"}, 192 | {file = "coverage-7.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db0338c4b0951d93d547e0ff8d8ea340fecf5885f5b00b23be5aa99549e14cfd"}, 193 | {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d31650d313bd90d027f4be7663dfa2241079edd780b56ac416b56eebe0a21aab"}, 194 | {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9437a4074b43c177c92c96d051957592afd85ba00d3e92002c8ef45ee75df438"}, 195 | {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9e17d9cb06c13b4f2ef570355fa45797d10f19ca71395910b249e3f77942a837"}, 196 | {file = "coverage-7.3.3-cp310-cp310-win32.whl", hash = "sha256:eee5e741b43ea1b49d98ab6e40f7e299e97715af2488d1c77a90de4a663a86e2"}, 197 | {file = "coverage-7.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:593efa42160c15c59ee9b66c5f27a453ed3968718e6e58431cdfb2d50d5ad284"}, 198 | {file = "coverage-7.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c944cf1775235c0857829c275c777a2c3e33032e544bcef614036f337ac37bb"}, 199 | {file = "coverage-7.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eda7f6e92358ac9e1717ce1f0377ed2b9320cea070906ece4e5c11d172a45a39"}, 200 | {file = "coverage-7.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c854c1d2c7d3e47f7120b560d1a30c1ca221e207439608d27bc4d08fd4aeae8"}, 201 | {file = "coverage-7.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:222b038f08a7ebed1e4e78ccf3c09a1ca4ac3da16de983e66520973443b546bc"}, 202 | {file = "coverage-7.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff4800783d85bff132f2cc7d007426ec698cdce08c3062c8d501ad3f4ea3d16c"}, 203 | {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fc200cec654311ca2c3f5ab3ce2220521b3d4732f68e1b1e79bef8fcfc1f2b97"}, 204 | {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:307aecb65bb77cbfebf2eb6e12009e9034d050c6c69d8a5f3f737b329f4f15fb"}, 205 | {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ffb0eacbadb705c0a6969b0adf468f126b064f3362411df95f6d4f31c40d31c1"}, 206 | {file = "coverage-7.3.3-cp311-cp311-win32.whl", hash = "sha256:79c32f875fd7c0ed8d642b221cf81feba98183d2ff14d1f37a1bbce6b0347d9f"}, 207 | {file = "coverage-7.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:243576944f7c1a1205e5cd658533a50eba662c74f9be4c050d51c69bd4532936"}, 208 | {file = "coverage-7.3.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a2ac4245f18057dfec3b0074c4eb366953bca6787f1ec397c004c78176a23d56"}, 209 | {file = "coverage-7.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f9191be7af41f0b54324ded600e8ddbcabea23e1e8ba419d9a53b241dece821d"}, 210 | {file = "coverage-7.3.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31c0b1b8b5a4aebf8fcd227237fc4263aa7fa0ddcd4d288d42f50eff18b0bac4"}, 211 | {file = "coverage-7.3.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee453085279df1bac0996bc97004771a4a052b1f1e23f6101213e3796ff3cb85"}, 212 | {file = "coverage-7.3.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1191270b06ecd68b1d00897b2daddb98e1719f63750969614ceb3438228c088e"}, 213 | {file = "coverage-7.3.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:007a7e49831cfe387473e92e9ff07377f6121120669ddc39674e7244350a6a29"}, 214 | {file = "coverage-7.3.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:af75cf83c2d57717a8493ed2246d34b1f3398cb8a92b10fd7a1858cad8e78f59"}, 215 | {file = "coverage-7.3.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:811ca7373da32f1ccee2927dc27dc523462fd30674a80102f86c6753d6681bc6"}, 216 | {file = "coverage-7.3.3-cp312-cp312-win32.whl", hash = "sha256:733537a182b5d62184f2a72796eb6901299898231a8e4f84c858c68684b25a70"}, 217 | {file = "coverage-7.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:e995efb191f04b01ced307dbd7407ebf6e6dc209b528d75583277b10fd1800ee"}, 218 | {file = "coverage-7.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fbd8a5fe6c893de21a3c6835071ec116d79334fbdf641743332e442a3466f7ea"}, 219 | {file = "coverage-7.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:50c472c1916540f8b2deef10cdc736cd2b3d1464d3945e4da0333862270dcb15"}, 220 | {file = "coverage-7.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e9223a18f51d00d3ce239c39fc41410489ec7a248a84fab443fbb39c943616c"}, 221 | {file = "coverage-7.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f501e36ac428c1b334c41e196ff6bd550c0353c7314716e80055b1f0a32ba394"}, 222 | {file = "coverage-7.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:475de8213ed95a6b6283056d180b2442eee38d5948d735cd3d3b52b86dd65b92"}, 223 | {file = "coverage-7.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afdcc10c01d0db217fc0a64f58c7edd635b8f27787fea0a3054b856a6dff8717"}, 224 | {file = "coverage-7.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fff0b2f249ac642fd735f009b8363c2b46cf406d3caec00e4deeb79b5ff39b40"}, 225 | {file = "coverage-7.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a1f76cfc122c9e0f62dbe0460ec9cc7696fc9a0293931a33b8870f78cf83a327"}, 226 | {file = "coverage-7.3.3-cp38-cp38-win32.whl", hash = "sha256:757453848c18d7ab5d5b5f1827293d580f156f1c2c8cef45bfc21f37d8681069"}, 227 | {file = "coverage-7.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad2453b852a1316c8a103c9c970db8fbc262f4f6b930aa6c606df9b2766eee06"}, 228 | {file = "coverage-7.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b15e03b8ee6a908db48eccf4e4e42397f146ab1e91c6324da44197a45cb9132"}, 229 | {file = "coverage-7.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:89400aa1752e09f666cc48708eaa171eef0ebe3d5f74044b614729231763ae69"}, 230 | {file = "coverage-7.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c59a3e59fb95e6d72e71dc915e6d7fa568863fad0a80b33bc7b82d6e9f844973"}, 231 | {file = "coverage-7.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ede881c7618f9cf93e2df0421ee127afdfd267d1b5d0c59bcea771cf160ea4a"}, 232 | {file = "coverage-7.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3bfd2c2f0e5384276e12b14882bf2c7621f97c35320c3e7132c156ce18436a1"}, 233 | {file = "coverage-7.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7f3bad1a9313401ff2964e411ab7d57fb700a2d5478b727e13f156c8f89774a0"}, 234 | {file = "coverage-7.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:65d716b736f16e250435473c5ca01285d73c29f20097decdbb12571d5dfb2c94"}, 235 | {file = "coverage-7.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a702e66483b1fe602717020a0e90506e759c84a71dbc1616dd55d29d86a9b91f"}, 236 | {file = "coverage-7.3.3-cp39-cp39-win32.whl", hash = "sha256:7fbf3f5756e7955174a31fb579307d69ffca91ad163467ed123858ce0f3fd4aa"}, 237 | {file = "coverage-7.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cad9afc1644b979211989ec3ff7d82110b2ed52995c2f7263e7841c846a75348"}, 238 | {file = "coverage-7.3.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:d299d379b676812e142fb57662a8d0d810b859421412b4d7af996154c00c31bb"}, 239 | {file = "coverage-7.3.3.tar.gz", hash = "sha256:df04c64e58df96b4427db8d0559e95e2df3138c9916c96f9f6a4dd220db2fdb7"}, 240 | ] 241 | 242 | [package.dependencies] 243 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 244 | 245 | [package.extras] 246 | toml = ["tomli"] 247 | 248 | [[package]] 249 | name = "exceptiongroup" 250 | version = "1.2.0" 251 | description = "Backport of PEP 654 (exception groups)" 252 | optional = false 253 | python-versions = ">=3.7" 254 | files = [ 255 | {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, 256 | {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, 257 | ] 258 | 259 | [package.extras] 260 | test = ["pytest (>=6)"] 261 | 262 | [[package]] 263 | name = "frozenlist" 264 | version = "1.4.1" 265 | description = "A list-like structure which implements collections.abc.MutableSequence" 266 | optional = false 267 | python-versions = ">=3.8" 268 | files = [ 269 | {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, 270 | {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, 271 | {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, 272 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, 273 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, 274 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, 275 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, 276 | {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, 277 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, 278 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, 279 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, 280 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, 281 | {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, 282 | {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, 283 | {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, 284 | {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, 285 | {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, 286 | {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, 287 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, 288 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, 289 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, 290 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, 291 | {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, 292 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, 293 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, 294 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, 295 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, 296 | {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, 297 | {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, 298 | {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, 299 | {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, 300 | {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, 301 | {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, 302 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, 303 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, 304 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, 305 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, 306 | {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, 307 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, 308 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, 309 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, 310 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, 311 | {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, 312 | {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, 313 | {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, 314 | {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, 315 | {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, 316 | {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, 317 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, 318 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, 319 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, 320 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, 321 | {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, 322 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, 323 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, 324 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, 325 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, 326 | {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, 327 | {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, 328 | {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, 329 | {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, 330 | {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, 331 | {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, 332 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, 333 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, 334 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, 335 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, 336 | {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, 337 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, 338 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, 339 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, 340 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, 341 | {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, 342 | {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, 343 | {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, 344 | {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, 345 | {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, 346 | ] 347 | 348 | [[package]] 349 | name = "gmqtt" 350 | version = "0.6.13" 351 | description = "Client for MQTT protocol" 352 | optional = false 353 | python-versions = ">=3.5" 354 | files = [ 355 | {file = "gmqtt-0.6.13-py3-none-any.whl", hash = "sha256:c67d4a646cc375a3e4623653a0ef3f2c84a7a8cfb029b5cdb58d20dc155c32fd"}, 356 | {file = "gmqtt-0.6.13.tar.gz", hash = "sha256:b4623d3e32ba266e96c04b0871bb057d05414371c1d279550648b390e6ba7577"}, 357 | ] 358 | 359 | [package.extras] 360 | test = ["atomicwrites (>=1.3.0)", "attrs (>=19.1.0)", "codecov (>=2.0.15)", "coverage (>=4.5.3)", "more-itertools (>=7.0.0)", "pluggy (>=0.11.0)", "py (>=1.8.0)", "pytest (>=5.4.0)", "pytest-asyncio (>=0.12.0)", "pytest-cov (>=2.7.1)", "six (>=1.12.0)", "uvloop (>=0.14.0)"] 361 | 362 | [[package]] 363 | name = "idna" 364 | version = "3.6" 365 | description = "Internationalized Domain Names in Applications (IDNA)" 366 | optional = false 367 | python-versions = ">=3.5" 368 | files = [ 369 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 370 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 371 | ] 372 | 373 | [[package]] 374 | name = "iniconfig" 375 | version = "2.0.0" 376 | description = "brain-dead simple config-ini parsing" 377 | optional = false 378 | python-versions = ">=3.7" 379 | files = [ 380 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 381 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 382 | ] 383 | 384 | [[package]] 385 | name = "multidict" 386 | version = "6.0.4" 387 | description = "multidict implementation" 388 | optional = false 389 | python-versions = ">=3.7" 390 | files = [ 391 | {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, 392 | {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, 393 | {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, 394 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, 395 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, 396 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, 397 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, 398 | {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, 399 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, 400 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, 401 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, 402 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, 403 | {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, 404 | {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, 405 | {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, 406 | {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, 407 | {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, 408 | {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, 409 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, 410 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, 411 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, 412 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, 413 | {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, 414 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, 415 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, 416 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, 417 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, 418 | {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, 419 | {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, 420 | {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, 421 | {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, 422 | {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, 423 | {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, 424 | {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, 425 | {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, 426 | {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, 427 | {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, 428 | {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, 429 | {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, 430 | {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, 431 | {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, 432 | {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, 433 | {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, 434 | {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, 435 | {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, 436 | {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, 437 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, 438 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, 439 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, 440 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, 441 | {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, 442 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, 443 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, 444 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, 445 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, 446 | {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, 447 | {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, 448 | {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, 449 | {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, 450 | {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, 451 | {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, 452 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, 453 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, 454 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, 455 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, 456 | {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, 457 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, 458 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, 459 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, 460 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, 461 | {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, 462 | {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, 463 | {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, 464 | {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, 465 | ] 466 | 467 | [[package]] 468 | name = "mypy" 469 | version = "1.7.1" 470 | description = "Optional static typing for Python" 471 | optional = false 472 | python-versions = ">=3.8" 473 | files = [ 474 | {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, 475 | {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, 476 | {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, 477 | {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, 478 | {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, 479 | {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, 480 | {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, 481 | {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, 482 | {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, 483 | {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, 484 | {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, 485 | {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, 486 | {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, 487 | {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, 488 | {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, 489 | {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, 490 | {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, 491 | {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, 492 | {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, 493 | {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, 494 | {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, 495 | {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, 496 | {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, 497 | {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, 498 | {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, 499 | {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, 500 | {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, 501 | ] 502 | 503 | [package.dependencies] 504 | mypy-extensions = ">=1.0.0" 505 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 506 | typing-extensions = ">=4.1.0" 507 | 508 | [package.extras] 509 | dmypy = ["psutil (>=4.0)"] 510 | install-types = ["pip"] 511 | mypyc = ["setuptools (>=50)"] 512 | reports = ["lxml"] 513 | 514 | [[package]] 515 | name = "mypy-extensions" 516 | version = "1.0.0" 517 | description = "Type system extensions for programs checked with the mypy type checker." 518 | optional = false 519 | python-versions = ">=3.5" 520 | files = [ 521 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 522 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 523 | ] 524 | 525 | [[package]] 526 | name = "packaging" 527 | version = "23.2" 528 | description = "Core utilities for Python packages" 529 | optional = false 530 | python-versions = ">=3.7" 531 | files = [ 532 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 533 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 534 | ] 535 | 536 | [[package]] 537 | name = "pluggy" 538 | version = "1.3.0" 539 | description = "plugin and hook calling mechanisms for python" 540 | optional = false 541 | python-versions = ">=3.8" 542 | files = [ 543 | {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, 544 | {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, 545 | ] 546 | 547 | [package.extras] 548 | dev = ["pre-commit", "tox"] 549 | testing = ["pytest", "pytest-benchmark"] 550 | 551 | [[package]] 552 | name = "pydantic" 553 | version = "1.10.13" 554 | description = "Data validation and settings management using python type hints" 555 | optional = false 556 | python-versions = ">=3.7" 557 | files = [ 558 | {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, 559 | {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, 560 | {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, 561 | {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, 562 | {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, 563 | {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, 564 | {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, 565 | {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, 566 | {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, 567 | {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, 568 | {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, 569 | {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, 570 | {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, 571 | {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, 572 | {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, 573 | {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, 574 | {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, 575 | {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, 576 | {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, 577 | {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, 578 | {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, 579 | {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, 580 | {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, 581 | {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, 582 | {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, 583 | {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, 584 | {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, 585 | {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, 586 | {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, 587 | {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, 588 | {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, 589 | {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, 590 | {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, 591 | {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, 592 | {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, 593 | {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, 594 | ] 595 | 596 | [package.dependencies] 597 | typing-extensions = ">=4.2.0" 598 | 599 | [package.extras] 600 | dotenv = ["python-dotenv (>=0.10.4)"] 601 | email = ["email-validator (>=1.0.3)"] 602 | 603 | [[package]] 604 | name = "pytest" 605 | version = "7.4.3" 606 | description = "pytest: simple powerful testing with Python" 607 | optional = false 608 | python-versions = ">=3.7" 609 | files = [ 610 | {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, 611 | {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, 612 | ] 613 | 614 | [package.dependencies] 615 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 616 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 617 | iniconfig = "*" 618 | packaging = "*" 619 | pluggy = ">=0.12,<2.0" 620 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 621 | 622 | [package.extras] 623 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 624 | 625 | [[package]] 626 | name = "pytest-asyncio" 627 | version = "0.15.1" 628 | description = "Pytest support for asyncio." 629 | optional = false 630 | python-versions = ">= 3.6" 631 | files = [ 632 | {file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"}, 633 | {file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"}, 634 | ] 635 | 636 | [package.dependencies] 637 | pytest = ">=5.4.0" 638 | 639 | [package.extras] 640 | testing = ["coverage", "hypothesis (>=5.7.1)"] 641 | 642 | [[package]] 643 | name = "pytest-cov" 644 | version = "4.1.0" 645 | description = "Pytest plugin for measuring coverage." 646 | optional = false 647 | python-versions = ">=3.7" 648 | files = [ 649 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 650 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 651 | ] 652 | 653 | [package.dependencies] 654 | coverage = {version = ">=5.2.1", extras = ["toml"]} 655 | pytest = ">=4.6" 656 | 657 | [package.extras] 658 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 659 | 660 | [[package]] 661 | name = "ruff" 662 | version = "0.1.8" 663 | description = "An extremely fast Python linter and code formatter, written in Rust." 664 | optional = false 665 | python-versions = ">=3.7" 666 | files = [ 667 | {file = "ruff-0.1.8-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7de792582f6e490ae6aef36a58d85df9f7a0cfd1b0d4fe6b4fb51803a3ac96fa"}, 668 | {file = "ruff-0.1.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8e3255afd186c142eef4ec400d7826134f028a85da2146102a1172ecc7c3696"}, 669 | {file = "ruff-0.1.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff78a7583020da124dd0deb835ece1d87bb91762d40c514ee9b67a087940528b"}, 670 | {file = "ruff-0.1.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd8ee69b02e7bdefe1e5da2d5b6eaaddcf4f90859f00281b2333c0e3a0cc9cd6"}, 671 | {file = "ruff-0.1.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a05b0ddd7ea25495e4115a43125e8a7ebed0aa043c3d432de7e7d6e8e8cd6448"}, 672 | {file = "ruff-0.1.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e6f08ca730f4dc1b76b473bdf30b1b37d42da379202a059eae54ec7fc1fbcfed"}, 673 | {file = "ruff-0.1.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f35960b02df6b827c1b903091bb14f4b003f6cf102705efc4ce78132a0aa5af3"}, 674 | {file = "ruff-0.1.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d076717c67b34c162da7c1a5bda16ffc205e0e0072c03745275e7eab888719f"}, 675 | {file = "ruff-0.1.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6a21ab023124eafb7cef6d038f835cb1155cd5ea798edd8d9eb2f8b84be07d9"}, 676 | {file = "ruff-0.1.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ce697c463458555027dfb194cb96d26608abab920fa85213deb5edf26e026664"}, 677 | {file = "ruff-0.1.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db6cedd9ffed55548ab313ad718bc34582d394e27a7875b4b952c2d29c001b26"}, 678 | {file = "ruff-0.1.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:05ffe9dbd278965271252704eddb97b4384bf58b971054d517decfbf8c523f05"}, 679 | {file = "ruff-0.1.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5daaeaf00ae3c1efec9742ff294b06c3a2a9db8d3db51ee4851c12ad385cda30"}, 680 | {file = "ruff-0.1.8-py3-none-win32.whl", hash = "sha256:e49fbdfe257fa41e5c9e13c79b9e79a23a79bd0e40b9314bc53840f520c2c0b3"}, 681 | {file = "ruff-0.1.8-py3-none-win_amd64.whl", hash = "sha256:f41f692f1691ad87f51708b823af4bb2c5c87c9248ddd3191c8f088e66ce590a"}, 682 | {file = "ruff-0.1.8-py3-none-win_arm64.whl", hash = "sha256:aa8ee4f8440023b0a6c3707f76cadce8657553655dcbb5fc9b2f9bb9bee389f6"}, 683 | {file = "ruff-0.1.8.tar.gz", hash = "sha256:f7ee467677467526cfe135eab86a40a0e8db43117936ac4f9b469ce9cdb3fb62"}, 684 | ] 685 | 686 | [[package]] 687 | name = "tomli" 688 | version = "2.0.1" 689 | description = "A lil' TOML parser" 690 | optional = false 691 | python-versions = ">=3.7" 692 | files = [ 693 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 694 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 695 | ] 696 | 697 | [[package]] 698 | name = "typing-extensions" 699 | version = "4.9.0" 700 | description = "Backported and Experimental Type Hints for Python 3.8+" 701 | optional = false 702 | python-versions = ">=3.8" 703 | files = [ 704 | {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, 705 | {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, 706 | ] 707 | 708 | [[package]] 709 | name = "yarl" 710 | version = "1.9.4" 711 | description = "Yet another URL library" 712 | optional = false 713 | python-versions = ">=3.7" 714 | files = [ 715 | {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, 716 | {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, 717 | {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, 718 | {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, 719 | {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, 720 | {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, 721 | {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, 722 | {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, 723 | {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, 724 | {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, 725 | {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, 726 | {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, 727 | {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, 728 | {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, 729 | {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, 730 | {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, 731 | {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, 732 | {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, 733 | {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, 734 | {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, 735 | {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, 736 | {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, 737 | {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, 738 | {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, 739 | {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, 740 | {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, 741 | {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, 742 | {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, 743 | {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, 744 | {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, 745 | {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, 746 | {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, 747 | {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, 748 | {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, 749 | {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, 750 | {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, 751 | {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, 752 | {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, 753 | {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, 754 | {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, 755 | {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, 756 | {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, 757 | {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, 758 | {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, 759 | {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, 760 | {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, 761 | {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, 762 | {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, 763 | {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, 764 | {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, 765 | {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, 766 | {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, 767 | {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, 768 | {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, 769 | {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, 770 | {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, 771 | {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, 772 | {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, 773 | {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, 774 | {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, 775 | {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, 776 | {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, 777 | {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, 778 | {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, 779 | {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, 780 | {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, 781 | {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, 782 | {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, 783 | {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, 784 | {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, 785 | {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, 786 | {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, 787 | {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, 788 | {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, 789 | {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, 790 | {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, 791 | {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, 792 | {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, 793 | {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, 794 | {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, 795 | {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, 796 | {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, 797 | {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, 798 | {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, 799 | {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, 800 | {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, 801 | {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, 802 | {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, 803 | {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, 804 | {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, 805 | ] 806 | 807 | [package.dependencies] 808 | idna = ">=2.0" 809 | multidict = ">=4.0" 810 | 811 | [metadata] 812 | lock-version = "2.0" 813 | python-versions = "^3.8" 814 | content-hash = "0be891aa61654bcac3a974d603f8211207d9f1e6565251715c620f7faf94a1d7" 815 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "hue2mqtt" 3 | version = "0.4.2" 4 | description = "Python Hue to MQTT Bridge" 5 | authors = ["Dan Trickey "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/trickeydan/hue2mqtt-python" 9 | homepage = "https://github.com/trickeydan/hue2mqtt-python" 10 | documentation = "https://github.com/trickeydan/hue2mqtt-python" 11 | classifiers = [ 12 | "Topic :: Software Development :: Libraries :: Python Modules", 13 | "Typing :: Typed", 14 | ] 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.8" 18 | gmqtt = "^0.6.10" 19 | aiohue = "^2.5.1" 20 | pydantic = "^1.9.2" 21 | click = "^8.1.3" 22 | aiohttp = "^3.8.1" 23 | tomli = { version = "^2.0.1", python = "<=3.11" } 24 | 25 | [tool.poetry.dev-dependencies] 26 | ruff = "*" 27 | mypy = "*" 28 | pytest = "*" 29 | pytest-cov = "*" 30 | pytest-asyncio = "^0.15.1" 31 | 32 | [tool.poetry.scripts] 33 | hue2mqtt = 'hue2mqtt.app:app' 34 | 35 | [build-system] 36 | requires = ["poetry>=0.12"] 37 | build-backend = "poetry.masonry.api" 38 | 39 | [tool.mypy] 40 | mypy_path = "stubs" 41 | 42 | warn_unused_ignores = true 43 | warn_return_any = true 44 | 45 | strict_optional = true 46 | no_implicit_optional = true 47 | 48 | disallow_subclassing_any = true 49 | disallow_any_generics = true 50 | 51 | disallow_untyped_calls = true 52 | disallow_untyped_defs = true 53 | disallow_incomplete_defs = true 54 | disallow_untyped_decorators = true 55 | 56 | check_untyped_defs = true 57 | 58 | [[tool.mypy.overrides]] 59 | module = ["gmqtt.*"] 60 | disallow_any_explicit = false 61 | 62 | [[tool.mypy.overrides]] 63 | module = ["aiohue.*"] 64 | ignore_missing_imports = true 65 | 66 | 67 | [tool.ruff] 68 | select = [ 69 | "A", 70 | "ANN", 71 | "B", 72 | "BLE", 73 | "C4", 74 | "COM", 75 | "DJ", 76 | "DTZ", 77 | "E", 78 | "F", 79 | "FBT", 80 | "I", 81 | "N", 82 | "S", 83 | "T10", 84 | "UP", 85 | "W", 86 | ] 87 | ignore = [ 88 | "ANN101", # Missing type annotation for `self` in method 89 | "ANN102", # Missing type annotation for `cls` in classmethod 90 | "ANN401", # Dynamically typed expressions (typing.Any) are disallowed 91 | "COM812", # 92 | "B009", # Do not call `getattr` with a constant attribute value. 93 | "B027", # an empty method in an abstract base class, but has no abstract decorator 94 | "FBT001", # Boolean positional arg in function definition 95 | "N805", # First argument of a method should be named `self` 96 | "S101", # S101 Use of `assert` detected 97 | ] 98 | 99 | target-version = "py38" 100 | line-length = 90 -------------------------------------------------------------------------------- /stubs/gmqtt/__init__.pyi: -------------------------------------------------------------------------------- 1 | """Stubs for gmqtt.""" 2 | 3 | from . import constants 4 | from .client import Client, Message, Subscription 5 | 6 | __all__ = [ 7 | 'Client', 8 | 'Message', 9 | 'Subscription', 10 | 'constants', 11 | ] -------------------------------------------------------------------------------- /stubs/gmqtt/client.pyi: -------------------------------------------------------------------------------- 1 | """Stubs for gmqtt.client.""" 2 | from typing import ( 3 | Any, 4 | Callable, 5 | Coroutine, 6 | Dict, 7 | List, 8 | Optional, 9 | Sequence, 10 | Tuple, 11 | Union, 12 | ) 13 | 14 | from .constants import PubRecReasonCode 15 | 16 | class Message: 17 | def __init__( 18 | self, 19 | topic: Union[str, bytes], 20 | payload: Optional[Union[List[Any], Tuple[Any, ...], Dict[Any, Any], int, float, str, bytes]], 21 | qos: int = 0, 22 | retain: bool = False, 23 | **kwargs: Any, 24 | ): ... 25 | 26 | class Subscription: 27 | def __init__( 28 | self, 29 | topic: Union[str, bytes], 30 | qos: int = 0, 31 | no_local: bool = False, 32 | retain_as_published: bool = False, 33 | retain_handling_options: int = 0, 34 | subscription_identifier: Optional[str] = None, 35 | ): ... 36 | 37 | class Client: 38 | 39 | _client_id: str 40 | 41 | def __init__( 42 | self, 43 | client_id: str, 44 | clean_session: bool = True, 45 | optimistic_acknowledgement: bool = True, 46 | will_message: Optional[Message] = None, 47 | **kwargs: Any, 48 | ): ... 49 | 50 | @property 51 | def is_connected(self) -> bool: ... 52 | 53 | @property 54 | def on_message(self) -> Callable[[Client, str, bytes, int, Dict[str, int]], Coroutine[Any, Any, PubRecReasonCode]]: ... 55 | 56 | @on_message.setter 57 | def on_message(self, f: Callable[[Client, str, bytes, int, Dict[str, int]], Coroutine[Any, Any, PubRecReasonCode]]) -> None: ... 58 | 59 | @property 60 | def on_connect(self) -> Callable[[Client, int, int, Dict[str, List[int]]], None]: ... 61 | 62 | @on_connect.setter 63 | def on_connect(self, f: Callable[[Client, int, int, Dict[str, List[int]]], None]) -> None: ... 64 | 65 | @property # type: ignore[misc] 66 | def on_connect(self) -> Callable[[Client, int, int, Dict[str, List[int]]], None]: ... 67 | 68 | @on_connect.setter 69 | def on_connect(self, f: Callable[[Client, int, int, Dict[str, List[int]]], None]) -> None: ... 70 | 71 | @property 72 | def on_disconnect(self) -> Callable[[Client, bytes], None]: ... 73 | 74 | @on_disconnect.setter 75 | def on_disconnect(self, f: Callable[[Client, bytes], None]) -> None: ... 76 | 77 | @property 78 | def reconnect_retries(self) -> int: ... 79 | 80 | @reconnect_retries.setter 81 | def reconnect_retries(self, n: int) -> None: ... 82 | 83 | 84 | def set_auth_credentials(self, username: str, password: Optional[str] = None) -> None: ... 85 | 86 | async def connect( 87 | self, 88 | host: str, 89 | port: int = 1883, 90 | ssl: bool = False, 91 | keepalive: int = 60, 92 | version: int = 5, 93 | raise_exc: bool = True, 94 | ) -> None: ... 95 | 96 | async def disconnect(self, reason_code: int = 0) -> None: ... 97 | 98 | def subscribe( 99 | self, 100 | subscription_or_topic: Union[str, Subscription, Sequence[Subscription]], 101 | qosint: int = 0, 102 | no_local: bool = False, 103 | retain_as_published: bool = False, 104 | retain_handling_optionsint: int = 0, 105 | )-> int: ... 106 | 107 | def publish( 108 | self, 109 | message_or_topic: Union[Message, str, bytes], 110 | payloadOptional: Optional[Union[List[Any], Tuple[Any, ...], Dict[Any, Any], int, float, str, bytes]] = None, 111 | qos: int = 0, 112 | retain: bool = False, 113 | ) -> None: ... 114 | -------------------------------------------------------------------------------- /stubs/gmqtt/constants.pyi: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | # MQTT protocol versions 4 | MQTTv311 = 4 5 | MQTTv50 = 5 6 | 7 | 8 | class PubRecReasonCode(enum.IntEnum): 9 | SUCCESS = 0 10 | NO_MATCHING_SUBSCRIBERS = 16 11 | UNSPECIFIED_ERROR = 128 12 | IMPLEMENTATION_SPECIFIC_ERROR = 131 13 | NOT_AUTHORIZED = 135 14 | TOPIC_NAME_INVALID = 144 15 | PACKET_IDENTIFIER_IN_USE = 145 16 | QUOTA_EXCEEDED = 151 17 | PAYLOAD_FORMAT_INVALID = 153 18 | -------------------------------------------------------------------------------- /tests/data/configs/valid.toml: -------------------------------------------------------------------------------- 1 | [mqtt] 2 | host = "::1" 3 | port = 1883 4 | enable_tls = false 5 | force_protocol_version_3_1 = true 6 | 7 | enable_auth = false 8 | username = "" 9 | password = "" 10 | 11 | topic_prefix = "hue2mqtt" 12 | 13 | [hue] 14 | ip = "192.168.1.100" # or IPv6: "[2001:db0::1]" 15 | username = "foo" -------------------------------------------------------------------------------- /tests/mqtt/test_topic.py: -------------------------------------------------------------------------------- 1 | """Tests for MQTT topic abstraction.""" 2 | 3 | from re import compile 4 | 5 | import pytest 6 | 7 | from hue2mqtt.mqtt import Topic 8 | 9 | BASIC_TOPICS = [ 10 | (["foo", "bar", "biz"], "foo/bar/biz"), 11 | (["superfoo", "12", "uberbar"], "superfoo/12/uberbar"), 12 | ] 13 | 14 | WILDCARD_TOPICS = [ 15 | (["foo", "#"], "foo/#", "foo/bar/biz"), 16 | (["superfoo", "+", "uberbar"], "superfoo/+/uberbar", "superfoo/12/uberbar"), 17 | ] 18 | 19 | 20 | def test_topic_init() -> None: 21 | """Test that we can construct Topic objects.""" 22 | for parts, _topic in BASIC_TOPICS: 23 | t = Topic(parts) 24 | assert isinstance(t, Topic) 25 | 26 | 27 | def test_topic_str() -> None: 28 | """Test the str render of a topic.""" 29 | for parts, topic in BASIC_TOPICS: 30 | t = Topic(parts) 31 | assert str(t) == topic 32 | 33 | for parts, topic, _ in WILDCARD_TOPICS: 34 | t = Topic(parts) 35 | assert str(t) == topic 36 | 37 | 38 | def test_topic_repr() -> None: 39 | """Test repr.""" 40 | t = Topic(["bees"]) 41 | assert repr(t) == 'Topic("bees")' 42 | 43 | 44 | def test_topic_equality() -> None: 45 | """Test topic equality.""" 46 | t = Topic(["bees"]) 47 | assert t == t 48 | assert t != 3 49 | assert t != "bees" 50 | assert t != Topic(["bees", "hive"]) 51 | 52 | 53 | def test_topic_hash() -> None: 54 | """Test that we can hash a topic.""" 55 | t = Topic(["bees"]) 56 | assert hash(t) is not None 57 | a = {} 58 | a[t] = "bees" 59 | assert a[t] == "bees" 60 | 61 | 62 | def test_topic_parse() -> None: 63 | """Test that we can parse topics.""" 64 | for parts, topic in BASIC_TOPICS: 65 | assert Topic(parts) == Topic.parse(topic) 66 | 67 | for parts, topic, _ in WILDCARD_TOPICS: 68 | assert Topic(parts) == Topic.parse(topic) 69 | 70 | 71 | def test_topic_parse_no_slash() -> None: 72 | """Test that preceeding and following slashes are rejected.""" 73 | cases = [ 74 | "foo/bar/", 75 | "/foo/bar", 76 | "/foo/bar/" "//", 77 | "/", 78 | "", 79 | ] 80 | 81 | for topic in cases: 82 | with pytest.raises(ValueError): 83 | Topic.parse(topic) 84 | 85 | 86 | def test_topic_is_publishable() -> None: 87 | """Test the is_publishable property.""" 88 | for parts, _ in BASIC_TOPICS: 89 | t = Topic(parts) 90 | assert t.is_publishable 91 | 92 | for parts, _, _ in WILDCARD_TOPICS: 93 | t = Topic(parts) 94 | assert not t.is_publishable 95 | 96 | 97 | def test_topic_regex() -> None: 98 | """Test the regex property.""" 99 | for parts, topic in BASIC_TOPICS: 100 | t = Topic(parts) 101 | assert t.regex == compile(f"^{topic}$") 102 | 103 | for parts, _, example in WILDCARD_TOPICS: 104 | t = Topic(parts) 105 | assert t.regex.match(example) 106 | assert not t.regex.match("u85932q4fds9/3£2####") 107 | 108 | 109 | def test_topic_match() -> None: 110 | """Test the match function.""" 111 | for parts, _topic, example in WILDCARD_TOPICS: 112 | t = Topic(parts) 113 | assert t.match(example) 114 | assert not t.match("u85932q4fds9/3£2####") 115 | -------------------------------------------------------------------------------- /tests/mqtt/test_wrapper.py: -------------------------------------------------------------------------------- 1 | """Test the MQTT Wrapper class.""" 2 | 3 | import asyncio 4 | from typing import Match 5 | 6 | import gmqtt 7 | import pytest 8 | from pydantic import BaseModel 9 | 10 | from hue2mqtt.config import MQTTBrokerInfo 11 | from hue2mqtt.mqtt.topic import Topic 12 | from hue2mqtt.mqtt.wrapper import MQTTWrapper 13 | 14 | BROKER_INFO = MQTTBrokerInfo( 15 | host="localhost", 16 | port=1883, 17 | ) 18 | 19 | 20 | class StubModel(BaseModel): 21 | """Test BaseModel.""" 22 | 23 | foo: str 24 | 25 | 26 | async def stub_message_handler( 27 | match: Match[str], 28 | payload: str, 29 | ) -> None: 30 | """Used in tests as a stub with the right type.""" 31 | pass 32 | 33 | 34 | def test_wrapper_init_minimal() -> None: 35 | """Test initialising the wrapper with minimal options.""" 36 | wr = MQTTWrapper("foo", BROKER_INFO) 37 | 38 | assert wr._client_name == "foo" 39 | assert wr._last_will is None 40 | 41 | assert len(wr._topic_handlers) == 0 42 | 43 | assert wr._client._client_id == "foo" 44 | 45 | 46 | def test_wrapper_is_connected_at_init() -> None: 47 | """Test that the wrapper is not connected to the broker at init.""" 48 | wr = MQTTWrapper("foo", BROKER_INFO) 49 | assert not wr.is_connected 50 | 51 | 52 | def test_wrapper_last_will_message_null() -> None: 53 | """Test that the last will message is None when not supplied.""" 54 | wr = MQTTWrapper("foo", BROKER_INFO) 55 | assert wr.last_will_message is None 56 | 57 | 58 | def test_wrapper_mqtt_prefix() -> None: 59 | """Test that the MQTT prefix is as expected.""" 60 | wr = MQTTWrapper("foo", BROKER_INFO) 61 | assert wr.mqtt_prefix == "hue2mqtt" 62 | 63 | 64 | def test_subscribe() -> None: 65 | """Test that subscribing works as expected.""" 66 | wr = MQTTWrapper("foo", BROKER_INFO) 67 | 68 | assert len(wr._topic_handlers) == 0 69 | 70 | wr.subscribe("bees/+", stub_message_handler) 71 | assert len(wr._topic_handlers) == 1 72 | assert wr._topic_handlers[Topic(["hue2mqtt", "bees", "+"])] == stub_message_handler 73 | 74 | 75 | @pytest.mark.filterwarnings("ignore") 76 | @pytest.mark.asyncio 77 | async def test_connect_disconnect() -> None: 78 | """Test that the wrapper can connect and disconnect from the broker.""" 79 | wr = MQTTWrapper("foo", BROKER_INFO) 80 | await wr.connect() 81 | assert wr.is_connected 82 | 83 | await wr.disconnect() 84 | assert not wr.is_connected 85 | 86 | 87 | @pytest.mark.asyncio 88 | async def test_handler_called() -> None: 89 | """Test that subscription handlers are called correctly.""" 90 | ev = asyncio.Event() 91 | 92 | async def test_handler( 93 | match: Match[str], 94 | payload: str, 95 | ) -> None: 96 | assert payload == "hive" 97 | ev.set() 98 | 99 | wr = MQTTWrapper("foo", BROKER_INFO) 100 | wr.subscribe("bees/+", test_handler) 101 | await wr.connect() 102 | 103 | # Manually call on_message 104 | res = await wr.on_message( 105 | wr._client, 106 | "hue2mqtt/bees/bar", 107 | b"hive", 108 | 0, 109 | {}, 110 | ) 111 | assert res == gmqtt.constants.PubRecReasonCode.SUCCESS 112 | 113 | await asyncio.wait_for(ev.wait(), 0.1) 114 | 115 | await wr.disconnect() 116 | 117 | 118 | @pytest.mark.asyncio 119 | async def test_publish_send_and_receive() -> None: 120 | """Test that we can publish and receive a message.""" 121 | ev = asyncio.Event() 122 | 123 | async def test_handler( 124 | match: Match[str], 125 | payload: str, 126 | ) -> None: 127 | ev.set() 128 | 129 | wr_sub = MQTTWrapper("foo", BROKER_INFO) 130 | wr_pub = MQTTWrapper("bar", BROKER_INFO) 131 | wr_sub.subscribe("bees/+", test_handler) 132 | await wr_sub.connect() 133 | await wr_pub.connect() 134 | 135 | wr_pub.publish("bees/foo", StubModel(foo="bar")) 136 | 137 | await asyncio.wait_for(ev.wait(), 0.5) 138 | 139 | await wr_sub.disconnect() 140 | await wr_pub.disconnect() 141 | 142 | 143 | @pytest.mark.asyncio 144 | async def test_publish_send_and_receive_on_self() -> None: 145 | """Test that we can publish and receive a message on it's own topic.""" 146 | ev = asyncio.Event() 147 | 148 | async def test_handler( 149 | match: Match[str], 150 | payload: str, 151 | ) -> None: 152 | ev.set() 153 | 154 | wr_sub = MQTTWrapper("foo", BROKER_INFO) 155 | wr_pub = MQTTWrapper("bar", BROKER_INFO) 156 | wr_sub.subscribe("", test_handler) 157 | await wr_sub.connect() 158 | await wr_pub.connect() 159 | 160 | wr_pub.publish("", StubModel(foo="bar")) 161 | 162 | await asyncio.wait_for(ev.wait(), 0.5) 163 | 164 | await wr_sub.disconnect() 165 | await wr_pub.disconnect() 166 | 167 | 168 | @pytest.mark.asyncio 169 | async def test_publish_bad_topic_error() -> None: 170 | """Test that we cannot publish to an invalid topic.""" 171 | wr_pub = MQTTWrapper("bar", BROKER_INFO) 172 | await wr_pub.connect() 173 | 174 | with pytest.raises(ValueError): 175 | wr_pub.publish("bees/+", StubModel(foo="bar")) 176 | 177 | with pytest.raises(ValueError): 178 | wr_pub.publish("bees/#", StubModel(foo="bar")) 179 | 180 | with pytest.raises(ValueError): 181 | wr_pub.publish("bees/", StubModel(foo="bar")) 182 | 183 | await wr_pub.disconnect() 184 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | """Test that we can load config files.""" 2 | 3 | from pathlib import Path 4 | 5 | from hue2mqtt.config import Hue2MQTTConfig 6 | 7 | DATA_DIR = Path(__file__).resolve().parent.joinpath("data/configs") 8 | 9 | 10 | def test_valid_config() -> None: 11 | """Test that we can load a valid config from a file.""" 12 | with DATA_DIR.joinpath("valid.toml").open("rb") as fh: 13 | config = Hue2MQTTConfig.load_from_file(fh) 14 | assert config is not None 15 | -------------------------------------------------------------------------------- /tests/test_module.py: -------------------------------------------------------------------------------- 1 | """Test that the hue2mqtt imports as expected.""" 2 | 3 | import hue2mqtt 4 | 5 | 6 | def test_module() -> None: 7 | """Test that the module behaves as expected.""" 8 | assert hue2mqtt.__version__ is not None 9 | --------------------------------------------------------------------------------