From d9618fabc3cf57a980fb7d9eddcc3b967503f1d9 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 4 Nov 2023 15:07:16 +0100 Subject: [PATCH] pyatmo v7.6.0 ------------- Added ===== - Opening category for NACamDoorTag - Schedule modification - Bticino MyHome Server 1 scopes - NLPD - Drivia dry contact - BTicino module stubs (functionality will come later) - support for Legrand garage door opener (NLJ) - support for BTicino intelligent light (BNIL) Removed ======= - Support for Python 3.8 and 3.9 Fixed ===== - Update functionality for NLP, NLC, NLT and NLG --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/publish-to-pypi.yml | 10 +- .github/workflows/publish-to-test-pypi.yml | 10 +- .github/workflows/pythonpackage.yml | 27 +- .github/workflows/release_gh.yml | 4 +- .gitignore | 2 +- .pre-commit-config.yaml | 47 +- .tool-versions | 1 + CHANGELOG.md | 29 +- CODEOWNERS | 5 + CODE_OF_CONDUCT.md | 128 ++ Pipfile | 7 +- Pipfile.lock | 1396 -------------------- README.md | 10 +- pyproject.toml | 121 +- setup.cfg | 6 +- src/pyatmo/__main__.py | 2 + src/pyatmo/account.py | 14 +- src/pyatmo/auth.py | 140 +- src/pyatmo/camera.py | 62 +- src/pyatmo/const.py | 9 +- src/pyatmo/event.py | 4 + src/pyatmo/exceptions.py | 20 +- src/pyatmo/helpers.py | 17 +- src/pyatmo/home.py | 102 +- src/pyatmo/home_coach.py | 20 +- src/pyatmo/modules/__init__.py | 31 +- src/pyatmo/modules/base_class.py | 25 +- src/pyatmo/modules/bticino.py | 40 + src/pyatmo/modules/device_types.py | 52 +- src/pyatmo/modules/idiamant.py | 8 + src/pyatmo/modules/legrand.py | 19 +- src/pyatmo/modules/module.py | 174 ++- src/pyatmo/modules/netatmo.py | 59 +- src/pyatmo/modules/somfy.py | 2 + src/pyatmo/person.py | 4 +- src/pyatmo/public_data.py | 83 +- src/pyatmo/room.py | 20 +- src/pyatmo/schedule.py | 44 +- src/pyatmo/thermostat.py | 51 +- src/pyatmo/weather_station.py | 42 +- tests/conftest.py | 5 +- tests/test_async.py | 21 +- tests/test_pyatmo.py | 3 +- tests/test_pyatmo_camera.py | 3 +- tests/test_pyatmo_homecoach.py | 3 +- tests/test_pyatmo_publicdata.py | 3 +- tests/test_pyatmo_refactor.py | 22 +- tests/test_pyatmo_thermostat.py | 2 +- tests/test_pyatmo_weatherstation.py | 3 +- tox.ini | 5 +- 51 files changed, 1198 insertions(+), 1721 deletions(-) create mode 100644 .tool-versions create mode 100644 CODEOWNERS create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 Pipfile.lock diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8c68fdeb..892dfcb4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 75454af8..07b15286 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -10,13 +10,13 @@ jobs: name: Build and publish 📦 to PyPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python 3.8 - uses: actions/setup-python@v4.4.0 + - name: Set up Python 3.10 + uses: actions/setup-python@v4.7.1 with: - python-version: 3.8 + python-version: 3.10.8 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -30,6 +30,6 @@ jobs: # password: ${{ secrets.PYPI_TEST_TOKEN }} # repository_url: https://test.pypi.org/legacy/ - name: Publish 📦 to PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.pypi_prod_token }} diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 1b15307e..92a08fc4 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -12,14 +12,14 @@ jobs: name: Build and publish 📦 to TestPyPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: development fetch-depth: 0 - - name: Set up Python 3.8 - uses: actions/setup-python@v4.4.0 + - name: Set up Python 3.10 + uses: actions/setup-python@v4.7.1 with: - python-version: 3.8 + python-version: 3.10.8 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -28,7 +28,7 @@ jobs: run: >- python -m build . - name: Publish 📦 to Test PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_TEST_TOKEN }} repository_url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index a4718c0f..c28b5721 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -16,14 +16,14 @@ jobs: strategy: max-parallel: 1 matrix: - python-version: [3.10.8] + python-version: [3.11.4] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.7.1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -39,36 +39,33 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.10.8] + python-version: [3.11.4] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.7.1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 - - name: Lint with flake8 + pip install ruff + - name: Lint with ruff run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + ruff check src/pyatmo build: runs-on: ubuntu-latest strategy: max-parallel: 4 matrix: - python-version: [3.8, 3.9, 3.10.8] + python-version: [3.10.8, 3.11.4] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.7.1 with: python-version: ${{ matrix.python-version }} - name: Run tests with tox diff --git a/.github/workflows/release_gh.yml b/.github/workflows/release_gh.yml index 4028e331..89b198b2 100644 --- a/.github/workflows/release_gh.yml +++ b/.github/workflows/release_gh.yml @@ -16,11 +16,13 @@ jobs: build: # The type of runner that the job will run on runs-on: ubuntu-latest + permissions: + contents: write # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.gitignore b/.gitignore index 02f199e6..07d2fb1b 100644 --- a/.gitignore +++ b/.gitignore @@ -180,7 +180,7 @@ target/ # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock +Pipfile.lock # celery beat schedule file celerybeat-schedule diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de8ec75b..3a65137d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,43 +2,42 @@ # "Version control integration" in README.md. default_stages: [commit, push] exclude: ^(fixtures/) -ci: - skip: [pylint] repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.292 + hooks: + - id: ruff + args: + - --fix + - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.15.0 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py310-plus] exclude: "external_src/int-tools" - repo: https://github.com/asottile/add-trailing-comma - rev: v2.4.0 + rev: v3.1.0 hooks: - id: add-trailing-comma args: [--py36-plus] exclude: "external_src/int-tools" - repo: https://github.com/asottile/yesqa - rev: v1.4.0 + rev: v1.5.0 hooks: - id: yesqa - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.9.1 hooks: - id: black language_version: python3 - - repo: https://github.com/pycqa/isort - rev: 5.11.4 - hooks: - - id: isort - name: isort (python) - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.5.1 hooks: - id: mypy name: mypy @@ -47,7 +46,7 @@ repos: - types-requests - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 # Use the ref you want to point at + rev: v4.5.0 # Use the ref you want to point at hooks: - id: check-ast - id: no-commit-to-branch @@ -62,24 +61,8 @@ repos: - id: debug-statements - id: check-toml - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 # pick a git hash / tag to point to - hooks: - - id: flake8 - exclude: (otp) - additional_dependencies: [flake8-typing-imports==1.14.0] - - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.2.0 + rev: v2.5.0 hooks: - id: setup-cfg-fmt args: [--include-version-classifiers] - - - repo: local - hooks: - - id: pylint - name: pylint - entry: pylint src tests - language: system - types: [python] - require_serial: true diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..c10ee4eb --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +python 3.11.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 270c2ad4..8c9fa85c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- ### Changed @@ -31,6 +30,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - +## [7.6.0] + +### Added + +- Opening category for NACamDoorTag +- Schedule modification +- Bticino MyHome Server 1 scopes +- NLPD - Drivia dry contact +- BTicino module stubs (functionality will come later) +- support for Legrand garage door opener (NLJ) +- support for BTicino intelligent light (BNIL) + +### Removed + +- Support for Python 3.8 and 3.9 + +### Fixed + +- Update functionality for NLP, NLC, NLT and NLG + ## [7.5.0] ### Added @@ -295,7 +314,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix crash when station name is not contained in the backend data -[unreleased]: https://github.com/jabesq/pyatmo/compare/v7.0.1...HEAD +[unreleased]: https://github.com/jabesq/pyatmo/compare/v7.5.0...HEAD +[7.5.0]: https://github.com/jabesq/pyatmo/compare/v7.4.0...v7.5.0 +[7.4.0]: https://github.com/jabesq/pyatmo/compare/v7.3.0...v7.4.0 +[7.3.0]: https://github.com/jabesq/pyatmo/compare/v7.2.0...v7.3.0 +[7.2.0]: https://github.com/jabesq/pyatmo/compare/v7.1.1...v7.2.0 +[7.1.1]: https://github.com/jabesq/pyatmo/compare/v7.1.0...v7.1.1 +[7.1.0]: https://github.com/jabesq/pyatmo/compare/v7.0.1...v7.1.0 [7.0.1]: https://github.com/jabesq/pyatmo/compare/v7.0.0...v7.0.1 [7.0.0]: https://github.com/jabesq/pyatmo/compare/v6.2.4...v7.0.0 [6.2.4]: https://github.com/jabesq/pyatmo/compare/v6.2.2...v6.2.4 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..b1621819 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,5 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @jabesq and @cgtobi will be requested for +# review when someone opens a pull request. +* @jabesq @cgtobi diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..18c91471 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Pipfile b/Pipfile index 95c5cdbb..4acfd440 100644 --- a/Pipfile +++ b/Pipfile @@ -5,20 +5,17 @@ verify_ssl = true [dev-packages] black = "*" -bleach = "~=5.0" +bleach = "~=6.1" docutils = "*" -flake8 = "*" time-machine = "*" -isort = "*" mypy = "*" pre-commit = "*" -pylint = "*" -pylint-pytest = "*" pytest = "*" pytest-asyncio = "*" pytest-cov = "*" pytest-mock = "*" requests-mock = "*" +ruff = "*" tox = ">=3.25" twine = "*" no-implicit-optional = "*" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index d4dbf6be..00000000 --- a/Pipfile.lock +++ /dev/null @@ -1,1396 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "016608ce6e0793be5ad7e7e962b91701b838dec60511263538587c9d3b78e84b" - }, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "aiohttp": { - "hashes": [ - "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8", - "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142", - "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18", - "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34", - "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a", - "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033", - "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06", - "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4", - "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d", - "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b", - "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc", - "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091", - "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d", - "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85", - "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb", - "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937", - "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf", - "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1", - "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b", - "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d", - "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269", - "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da", - "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346", - "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494", - "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697", - "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4", - "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585", - "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c", - "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da", - "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad", - "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2", - "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6", - "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c", - "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849", - "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa", - "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b", - "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb", - "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7", - "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715", - "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76", - "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d", - "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276", - "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6", - "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37", - "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb", - "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d", - "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c", - "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446", - "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008", - "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342", - "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d", - "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7", - "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061", - "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba", - "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7", - "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290", - "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0", - "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d", - "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8", - "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f", - "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48", - "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502", - "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62", - "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9", - "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403", - "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77", - "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476", - "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e", - "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96", - "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5", - "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784", - "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091", - "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b", - "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97", - "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a", - "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2", - "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9", - "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d", - "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73", - "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017", - "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363", - "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c", - "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d", - "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618", - "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491", - "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b", - "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca" - ], - "index": "pypi", - "version": "==3.8.3" - }, - "aiosignal": { - "hashes": [ - "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", - "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "async-timeout": { - "hashes": [ - "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", - "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.2" - }, - "attrs": { - "hashes": [ - "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", - "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" - ], - "markers": "python_version >= '3.6'", - "version": "==22.2.0" - }, - "certifi": { - "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.12.7" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" - ], - "markers": "python_version >= '3.6'", - "version": "==2.1.1" - }, - "frozenlist": { - "hashes": [ - "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c", - "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f", - "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a", - "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784", - "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27", - "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d", - "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3", - "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678", - "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a", - "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483", - "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8", - "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf", - "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99", - "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c", - "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48", - "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5", - "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56", - "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e", - "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1", - "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401", - "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4", - "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e", - "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649", - "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a", - "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d", - "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0", - "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6", - "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d", - "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b", - "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6", - "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf", - "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef", - "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7", - "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842", - "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba", - "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420", - "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b", - "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d", - "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332", - "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936", - "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816", - "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91", - "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420", - "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448", - "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411", - "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4", - "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32", - "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b", - "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0", - "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530", - "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669", - "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7", - "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1", - "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5", - "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce", - "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4", - "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e", - "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2", - "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d", - "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9", - "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642", - "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0", - "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703", - "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb", - "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1", - "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13", - "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab", - "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38", - "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb", - "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb", - "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81", - "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8", - "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd", - "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.3" - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" - }, - "multidict": { - "hashes": [ - "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9", - "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8", - "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03", - "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710", - "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161", - "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664", - "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569", - "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067", - "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313", - "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706", - "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2", - "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636", - "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49", - "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93", - "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603", - "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0", - "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60", - "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4", - "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e", - "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1", - "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60", - "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951", - "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc", - "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe", - "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95", - "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d", - "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8", - "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed", - "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2", - "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775", - "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87", - "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c", - "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2", - "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98", - "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3", - "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe", - "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78", - "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660", - "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176", - "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e", - "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988", - "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c", - "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c", - "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0", - "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449", - "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f", - "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde", - "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5", - "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d", - "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac", - "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a", - "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9", - "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca", - "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11", - "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35", - "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063", - "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b", - "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982", - "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258", - "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1", - "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52", - "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480", - "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7", - "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461", - "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d", - "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc", - "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779", - "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a", - "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547", - "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0", - "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171", - "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf", - "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d", - "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0.4" - }, - "oauthlib": { - "hashes": [ - "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", - "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" - ], - "markers": "python_version >= '3.6'", - "version": "==3.2.2" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "index": "pypi", - "version": "==2.28.1" - }, - "requests-oauthlib": { - "hashes": [ - "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", - "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a" - ], - "index": "pypi", - "version": "==1.3.1" - }, - "urllib3": { - "hashes": [ - "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", - "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.13" - }, - "yarl": { - "hashes": [ - "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87", - "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89", - "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a", - "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08", - "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996", - "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077", - "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901", - "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e", - "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee", - "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574", - "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165", - "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634", - "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229", - "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b", - "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f", - "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7", - "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf", - "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89", - "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0", - "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1", - "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe", - "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf", - "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76", - "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951", - "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863", - "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06", - "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562", - "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6", - "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c", - "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e", - "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1", - "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3", - "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3", - "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778", - "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8", - "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2", - "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b", - "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d", - "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f", - "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c", - "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581", - "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918", - "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c", - "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e", - "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220", - "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37", - "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739", - "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77", - "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6", - "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42", - "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946", - "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5", - "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d", - "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146", - "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a", - "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83", - "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef", - "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80", - "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588", - "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5", - "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2", - "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef", - "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826", - "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05", - "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516", - "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0", - "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4", - "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2", - "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0", - "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd", - "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8", - "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b", - "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1", - "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c" - ], - "markers": "python_version >= '3.7'", - "version": "==1.8.2" - } - }, - "develop": { - "astroid": { - "hashes": [ - "sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907", - "sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7" - ], - "markers": "python_full_version >= '3.7.2'", - "version": "==2.12.13" - }, - "attrs": { - "hashes": [ - "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", - "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" - ], - "markers": "python_version >= '3.6'", - "version": "==22.2.0" - }, - "black": { - "hashes": [ - "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320", - "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351", - "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350", - "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f", - "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf", - "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148", - "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4", - "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d", - "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc", - "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d", - "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2", - "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f" - ], - "index": "pypi", - "version": "==22.12.0" - }, - "bleach": { - "hashes": [ - "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a", - "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c" - ], - "index": "pypi", - "version": "==5.0.1" - }, - "cachetools": { - "hashes": [ - "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757", - "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db" - ], - "markers": "python_version ~= '3.7'", - "version": "==5.2.0" - }, - "certifi": { - "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" - ], - "markers": "python_version >= '3.6'", - "version": "==2022.12.7" - }, - "cffi": { - "hashes": [ - "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", - "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", - "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", - "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", - "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", - "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", - "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", - "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", - "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", - "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", - "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", - "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", - "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", - "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", - "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", - "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", - "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", - "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", - "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", - "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", - "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", - "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", - "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", - "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", - "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", - "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", - "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", - "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", - "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", - "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", - "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", - "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", - "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", - "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", - "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", - "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", - "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", - "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", - "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", - "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", - "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", - "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", - "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", - "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", - "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", - "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", - "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", - "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", - "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", - "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", - "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", - "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", - "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", - "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", - "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", - "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", - "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", - "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", - "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", - "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", - "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", - "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", - "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", - "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" - ], - "version": "==1.15.1" - }, - "cfgv": { - "hashes": [ - "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", - "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" - ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.3.1" - }, - "chardet": { - "hashes": [ - "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5", - "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9" - ], - "markers": "python_version >= '3.7'", - "version": "==5.1.0" - }, - "charset-normalizer": { - "hashes": [ - "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", - "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" - ], - "markers": "python_version >= '3.6'", - "version": "==2.1.1" - }, - "click": { - "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.3" - }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==0.4.6" - }, - "commonmark": { - "hashes": [ - "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", - "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" - ], - "version": "==0.9.1" - }, - "coverage": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:07bcfb1d8ac94af886b54e18a88b393f6a73d5959bb31e46644a02453c36e475", - "sha256:09f6b5a8415b6b3e136d5fec62b552972187265cb705097bf030eb9d4ffb9b60", - "sha256:0a79137fc99815fff6a852c233628e735ec15903cfd16da0f229d9c4d45926ab", - "sha256:0b4b3a4d9915b2be879aff6299c0a6129f3d08a775d5a061f503cf79571f73e4", - "sha256:1285648428a6101b5f41a18991c84f1c3959cee359e51b8375c5882fc364a13f", - "sha256:12a5aa77783d49e05439fbe6e6b427484f8a0f9f456b46a51d8aac022cfd024d", - "sha256:19ec666533f0f70a0993f88b8273057b96c07b9d26457b41863ccd021a043b9a", - "sha256:1e414dc32ee5c3f36544ea466b6f52f28a7af788653744b8570d0bf12ff34bc0", - "sha256:2c44fcfb3781b41409d0f060a4ed748537557de9362a8a9282182fafb7a76ab4", - "sha256:397b4a923cc7566bbc7ae2dfd0ba5a039b61d19c740f1373791f2ebd11caea59", - "sha256:3cfc595d2af13856505631be072835c59f1acf30028d1c860b435c5fc9c15b69", - "sha256:3dd4ee135e08037f458425b8842d24a95a0961831a33f89685ff86b77d378f89", - "sha256:486ee81fa694b4b796fc5617e376326a088f7b9729c74d9defa211813f3861e4", - "sha256:4f943a3b2bc520102dd3e0bb465e1286e12c9a54f58accd71b9e65324d9c7c01", - "sha256:63d56165a7c76265468d7e0c5548215a5ba515fc2cba5232d17df97bffa10f6c", - "sha256:66b18c3cf8bbab0cce0d7b9e4262dc830e93588986865a8c78ab2ae324b3ed56", - "sha256:691571f31ace1837838b7e421d3a09a8c00b4aac32efacb4fc9bd0a5c647d25a", - "sha256:6c5ad996c6fa4d8ed669cfa1e8551348729d008a2caf81489ab9ea67cfbc7498", - "sha256:6d55d840e1b8c0002fce66443e124e8581f30f9ead2e54fbf6709fb593181f2c", - "sha256:72d1507f152abacea81f65fee38e4ef3ac3c02ff8bc16f21d935fd3a8a4ad910", - "sha256:74f70cd92669394eaf8d7756d1b195c8032cf7bbbdfce3bc489d4e15b3b8cf73", - "sha256:830525361249dc4cd013652b0efad645a385707a5ae49350c894b67d23fbb07c", - "sha256:854f22fa361d1ff914c7efa347398374cc7d567bdafa48ac3aa22334650dfba2", - "sha256:89caf4425fe88889e2973a8e9a3f6f5f9bbe5dd411d7d521e86428c08a873a4a", - "sha256:9158f8fb06747ac17bd237930c4372336edc85b6e13bdc778e60f9d685c3ca37", - "sha256:92651580bd46519067e36493acb394ea0607b55b45bd81dd4e26379ed1871f55", - "sha256:978258fec36c154b5e250d356c59af7d4c3ba02bef4b99cda90b6029441d797d", - "sha256:9823e4789ab70f3ec88724bba1a203f2856331986cd893dedbe3e23a6cfc1e4e", - "sha256:9b373c9345c584bb4b5f5b8840df7f4ab48c4cbb7934b58d52c57020d911b856", - "sha256:a4a574a19eeb67575a5328a5760bbbb737faa685616586a9f9da4281f940109c", - "sha256:aec2d1515d9d39ff270059fd3afbb3b44e6ec5758af73caf18991807138c7118", - "sha256:b3695c4f4750bca943b3e1f74ad4be8d29e4aeab927d50772c41359107bd5d5c", - "sha256:b3763e7fcade2ff6c8e62340af9277f54336920489ceb6a8cd6cc96da52fcc62", - "sha256:b66bb21a23680dee0be66557dc6b02a3152ddb55edf9f6723fa4a93368f7158d", - "sha256:b6f22bb64cc39bcb883e5910f99a27b200fdc14cdd79df8696fa96b0005c9444", - "sha256:b77015d1cb8fe941be1222a5a8b4e3fbca88180cfa7e2d4a4e58aeabadef0ab7", - "sha256:b9ea158775c7c2d3e54530a92da79496fb3fb577c876eec761c23e028f1e216c", - "sha256:c20cfebcc149a4c212f6491a5f9ff56f41829cd4f607b5be71bb2d530ef243b1", - "sha256:cfded268092a84605f1cc19e5c737f9ce630a8900a3589e9289622db161967e9", - "sha256:d1991f1dd95eba69d2cd7708ff6c2bbd2426160ffc73c2b81f617a053ebcb1a8", - "sha256:d3022c3007d3267a880b5adcf18c2a9bf1fc64469b394a804886b401959b8742", - "sha256:d6814854c02cbcd9c873c0f3286a02e3ac1250625cca822ca6bc1018c5b19f1c", - "sha256:d87717959d4d0ee9db08a0f1d80d21eb585aafe30f9b0a54ecf779a69cb015f6", - "sha256:e00c14720b8b3b6c23b487e70bd406abafc976ddc50490f645166f111c419c39", - "sha256:e60bef2e2416f15fdc05772bf87db06c6a6f9870d1db08fdd019fbec98ae24a9", - "sha256:e78e9dcbf4f3853d3ae18a8f9272111242531535ec9e1009fa8ec4a2b74557dc", - "sha256:f66460f17c9319ea4f91c165d46840314f0a7c004720b20be58594d162a441d8", - "sha256:fa6a5a224b7f4cfb226f4fc55a57e8537fcc096f42219128c2c74c0e7d0953e1", - "sha256:fb992c47cb1e5bd6a01e97182400bcc2ba2077080a17fcd7be23aaa6e572e390", - "sha256:fd1b9c5adc066db699ccf7fa839189a649afcdd9e02cb5dc9d24e67e7922737d", - "sha256:fd556ff16a57a070ce4f31c635953cc44e25244f91a0378c6e9bdfd40fdb249f" - ], - "markers": "python_version >= '3.7'", - "version": "==7.0.1" - }, - "cryptography": { - "hashes": [ - "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd", - "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db", - "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290", - "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744", - "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb", - "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d", - "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70", - "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b", - "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876", - "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083", - "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6", - "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1", - "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00", - "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b", - "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b", - "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285", - "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9", - "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0", - "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d", - "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2", - "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8", - "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee", - "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b", - "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7", - "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353", - "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c" - ], - "markers": "python_version >= '3.6'", - "version": "==38.0.4" - }, - "dill": { - "hashes": [ - "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0", - "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373" - ], - "markers": "python_version >= '3.11'", - "version": "==0.3.6" - }, - "distlib": { - "hashes": [ - "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46", - "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e" - ], - "version": "==0.3.6" - }, - "docutils": { - "hashes": [ - "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6", - "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc" - ], - "index": "pypi", - "version": "==0.19" - }, - "filelock": { - "hashes": [ - "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de", - "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d" - ], - "markers": "python_version >= '3.7'", - "version": "==3.9.0" - }, - "flake8": { - "hashes": [ - "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7", - "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181" - ], - "index": "pypi", - "version": "==6.0.0" - }, - "identify": { - "hashes": [ - "sha256:14b7076b29c99b1b0b8b08e96d448c7b877a9b07683cd8cfda2ea06af85ffa1c", - "sha256:e7db36b772b188099616aaf2accbee122949d1c6a1bac4f38196720d6f9f06db" - ], - "markers": "python_version >= '3.7'", - "version": "==2.5.11" - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" - }, - "importlib-metadata": { - "hashes": [ - "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", - "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0.0" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "isort": { - "hashes": [ - "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6", - "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b" - ], - "index": "pypi", - "version": "==5.11.4" - }, - "jaraco.classes": { - "hashes": [ - "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158", - "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a" - ], - "markers": "python_version >= '3.7'", - "version": "==3.2.3" - }, - "jeepney": { - "hashes": [ - "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", - "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.8.0" - }, - "keyring": { - "hashes": [ - "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd", - "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678" - ], - "markers": "python_version >= '3.7'", - "version": "==23.13.1" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada", - "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d", - "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7", - "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe", - "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd", - "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c", - "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858", - "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288", - "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec", - "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f", - "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891", - "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c", - "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25", - "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156", - "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8", - "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f", - "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e", - "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0", - "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b" - ], - "markers": "python_version >= '3.7'", - "version": "==1.8.0" - }, - "libcst": { - "hashes": [ - "sha256:01786c403348f76f274dbaf3888ae237ffb73e6ed6973e65eba5c1fc389861dd", - "sha256:045b3b0b06413cdae6e9751b5f417f789ffa410f2cb2815e3e0e0ea6bef10ec0", - "sha256:10479371d04ee8dc978c889c1774bbf6a83df88fa055fcb0159a606f6679c565", - "sha256:1266530bf840cc40633a04feb578bb4cac1aa3aea058cc3729e24eab09a8e996", - "sha256:132bec627b064bd567e7e4cd6c89524d02842151eb0d8f5f3f7ffd2579ec1b09", - "sha256:1350d375d3fb9b20a6cf10c09b2964baca9be753a033dde7c1aced49d8e58387", - "sha256:15ded11ff7f4572f91635e02b519ae959f782689fdb4445bbebb7a3cc5c71d75", - "sha256:183636141b839aa35b639e100883813744523bc7c12528906621121731b28443", - "sha256:27be8db54c0e5fe440021a771a38b81a7dbc23cd630eb8b0e9828b7717f9b702", - "sha256:3822056dc13326082362db35b3f649e0f4a97e36ddb4e487441da8e0fb9db7b3", - "sha256:3cf48d7aec6dc54b02aec0b1bb413c5bb3b02d852fd6facf1f05c7213e61a176", - "sha256:400166fc4efb9aa06ce44498d443aa78519082695b1894202dd73cd507d2d712", - "sha256:46123863fba35cc84f7b54dd68826419cabfd9504d8a101c7fe3313ea03776f9", - "sha256:4f9e42085c403e22201e5c41e707ef73e4ea910ad9fc67983ceee2368097f54e", - "sha256:596860090aeed3ee6ad1e59c35c6c4110a57e4e896abf51b91cae003ec720a11", - "sha256:5b266867b712a120fad93983de432ddb2ccb062eb5fd2bea748c9a94cb200c36", - "sha256:7415569ab998a85b0fc9af3a204611ea7fadb2d719a12532c448f8fc98f5aca4", - "sha256:76491f67431318c3145442e97dddcead7075b074c59eac51be7cc9e3fffec6ee", - "sha256:786e562b54bbcd17a060d1244deeef466b7ee07fe544074c252c4a169e38f1ee", - "sha256:794250d2359edd518fb698e5d21c38a5bdfc5e4a75d0407b4c19818271ce6742", - "sha256:7a98286cbbfa90a42d376900c875161ad02a5a2a6b7c94c0f7afd9075e329ce4", - "sha256:7e33b66762efaa014c38819efae5d8f726dd823e32d5d691035484411d2a2a69", - "sha256:9b3348c6b7711a5235b133bd8e11d22e903c388db42485b8ceb5f2aa0fae9b9f", - "sha256:aa53993e9a2853efb3ed3605da39f2e7125df6430f613eb67ef886c1ce4f94b5", - "sha256:d67bc87e0d8db9434f2ea063734938a320f541f4c6da1074001e372f840f385d", - "sha256:e316da5a126f2a9e1d7680f95f907b575f082a35e2f8bd5620c59b2aaaebfe0a", - "sha256:e799add8fba4976628b9c1a6768d73178bf898f0ed1bd1322930c2d3db9063ba", - "sha256:f4487608258109f774300466d4ca97353df29ae6ac23d1502e13e5509423c9d5", - "sha256:f6ce794483d4c605ef0f5b199a49fb6996f9586ca938b7bfef213bd13858d7ab", - "sha256:f9679177391ccb9b0cdde3185c22bf366cb672457c4b7f4031fcb3b5e739fbd6" - ], - "markers": "python_version >= '3.7'", - "version": "==0.4.9" - }, - "mccabe": { - "hashes": [ - "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, - "more-itertools": { - "hashes": [ - "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41", - "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab" - ], - "markers": "python_version >= '3.7'", - "version": "==9.0.0" - }, - "mypy": { - "hashes": [ - "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d", - "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6", - "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf", - "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f", - "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813", - "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33", - "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad", - "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05", - "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297", - "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06", - "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd", - "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243", - "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305", - "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476", - "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711", - "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70", - "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5", - "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461", - "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab", - "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c", - "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d", - "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135", - "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93", - "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648", - "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a", - "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb", - "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3", - "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372", - "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb", - "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef" - ], - "index": "pypi", - "version": "==0.991" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "no-implicit-optional": { - "hashes": [ - "sha256:ac9f2a93f36f69a66437eb14b74efc9a0c177cd02494a38de99f75856aa618d9", - "sha256:b2487aa8e4d6a4fdb90de2dfeb0cc03b68c771290d90a70c9fd7239b2fd47be8" - ], - "index": "pypi", - "version": "==1.3" - }, - "nodeenv": { - "hashes": [ - "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e", - "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==1.7.0" - }, - "packaging": { - "hashes": [ - "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3", - "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3" - ], - "markers": "python_version >= '3.7'", - "version": "==22.0" - }, - "pathspec": { - "hashes": [ - "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6", - "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6" - ], - "markers": "python_version >= '3.7'", - "version": "==0.10.3" - }, - "pkginfo": { - "hashes": [ - "sha256:ac03e37e4d601aaee40f8087f63fc4a2a6c9814dda2c8fa6aab1b1829653bdfa", - "sha256:d580059503f2f4549ad6e4c106d7437356dbd430e2c7df99ee1efe03d75f691e" - ], - "markers": "python_version >= '3.6'", - "version": "==1.9.2" - }, - "platformdirs": { - "hashes": [ - "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490", - "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2" - ], - "markers": "python_version >= '3.7'", - "version": "==2.6.2" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "pre-commit": { - "hashes": [ - "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658", - "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad" - ], - "index": "pypi", - "version": "==2.21.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053", - "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610" - ], - "markers": "python_version >= '3.6'", - "version": "==2.10.0" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "version": "==2.21" - }, - "pyflakes": { - "hashes": [ - "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf", - "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" - }, - "pygments": { - "hashes": [ - "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297", - "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717" - ], - "markers": "python_version >= '3.6'", - "version": "==2.14.0" - }, - "pylint": { - "hashes": [ - "sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4", - "sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb" - ], - "index": "pypi", - "version": "==2.15.9" - }, - "pylint-pytest": { - "hashes": [ - "sha256:fb20ef318081cee3d5febc631a7b9c40fa356b05e4f769d6e60a337e58c8879b" - ], - "index": "pypi", - "version": "==1.1.2" - }, - "pyproject-api": { - "hashes": [ - "sha256:093c047d192ceadcab7afd6b501276bf2ce44adf41cb9c313234518cddd20818", - "sha256:155d5623453173b7b4e9379a3146ccef2d52335234eb2d03d6ba730e7dad179c" - ], - "markers": "python_version >= '3.7'", - "version": "==1.2.1" - }, - "pytest": { - "hashes": [ - "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71", - "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59" - ], - "index": "pypi", - "version": "==7.2.0" - }, - "pytest-asyncio": { - "hashes": [ - "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36", - "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442" - ], - "index": "pypi", - "version": "==0.20.3" - }, - "pytest-cov": { - "hashes": [ - "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b", - "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470" - ], - "index": "pypi", - "version": "==4.0.0" - }, - "pytest-mock": { - "hashes": [ - "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b", - "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f" - ], - "index": "pypi", - "version": "==3.10.0" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "pyyaml": { - "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0" - }, - "readme-renderer": { - "hashes": [ - "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273", - "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343" - ], - "markers": "python_version >= '3.7'", - "version": "==37.3" - }, - "requests": { - "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" - ], - "index": "pypi", - "version": "==2.28.1" - }, - "requests-mock": { - "hashes": [ - "sha256:2fdbb637ad17ee15c06f33d31169e71bf9fe2bdb7bc9da26185be0dd8d842699", - "sha256:59c9c32419a9fb1ae83ec242d98e889c45bd7d7a65d48375cc243ec08441658b" - ], - "index": "pypi", - "version": "==1.10.0" - }, - "requests-toolbelt": { - "hashes": [ - "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7", - "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.1" - }, - "rfc3986": { - "hashes": [ - "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", - "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "rich": { - "hashes": [ - "sha256:12b1d77ee7edf251b741531323f0d990f5f570a4e7c054d0bfb59fb7981ad977", - "sha256:3aa9eba7219b8c575c6494446a59f702552efe1aa261e7eeb95548fa586e1950" - ], - "markers": "python_version >= '3.7'", - "version": "==13.0.0" - }, - "secretstorage": { - "hashes": [ - "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", - "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.3.3" - }, - "setuptools": { - "hashes": [ - "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54", - "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75" - ], - "markers": "python_version >= '3.7'", - "version": "==65.6.3" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "time-machine": { - "hashes": [ - "sha256:010a58a8de1120308befae19e6c9de2ef5ca5206635cea33cb264998725cc027", - "sha256:0b9c36240876622b7f2f9e11bf72f100857c0a1e1a59af2da3d5067efea62c37", - "sha256:1d0ab46ce8a60baf9d86525694bf698fed9efefd22b8cbe1ca3e74abbb3239e1", - "sha256:2f080f6f7ca8cfca43bc5639288aebd0a273b4b5bd0acff609c2318728b13a18", - "sha256:359c806e5b9a7a3c73dbb808d19dca297f5504a5eefdc5d031db8d918f43e364", - "sha256:36dde844d28549929fab171d683c28a8db1c206547bcf6b7aca77319847d2046", - "sha256:372a97da01db89533d2f4ce50bbd908e5c56df7b8cfd6a005b177d0b14dc2938", - "sha256:3ce445775fcf7cb4040cfdba4b7c4888e7fd98bbcccfe1dc3fa8a798ed1f1d24", - "sha256:3ff5148e2e73392db8418a1fe2f0b06f4a0e76772933502fb61e4c3000b5324e", - "sha256:49df5eea2160068e5b2bf28c22fc4c5aea00862ad88ddc3b62fc0f0683e97538", - "sha256:4b55654aaeaba380fcd6c004b8ada2978fdd4ece1e61e6b9717c6d4cc7fbbcd9", - "sha256:4f3755d9342ca1f1019418db52072272dfd75eb818fa4726fa8aabe208b38c26", - "sha256:5657e0e6077cf15b37f0d8cf78e868113bbb3ecccc60064c40fe52d8166ca8b1", - "sha256:60222d43f6e93a926adc36ed37a54bc8e4d0d8d1c4d449096afcfe85086129c2", - "sha256:6211beee9f5dace08b1bbbb1fb09e34a69c52d87eea676729f14c8660481dff6", - "sha256:6463e302c96eb8c691c4340e281bd54327a213b924fa189aea81accf7e7f78df", - "sha256:68ec8b83197db32c7a12da5f6b83c91271af3ed7f5dc122d2900a8de01dff9f0", - "sha256:69898aed9b2315a90f5855343d9aa34d05fa06032e2e3bb14f2528941ec89dc1", - "sha256:6b632d60aa0883dc7292ac3d32050604d26ec2bbd5c4d42fb0de3b4ef17343e2", - "sha256:728263611d7940fda34d21573bd2b3f1491bdb52dbf75c5fe6c226dfe4655201", - "sha256:748d701228e646c224f2adfa6a11b986cd4aa90f1b8c13ef4534a3919c796bc0", - "sha256:8367fd03f2d7349c7fc20f14de186974eaca2502c64b948212de663742c8fd11", - "sha256:8670cb5cfda99f483d60de6ce56ceb0ec5d359193e79e4688e1c3c9db3937383", - "sha256:8830510adbf0a231184da277db9de1d55ef93ed228a575d217aaee295505abf1", - "sha256:8976b7b1f7de13598b655d459f5640f90f3cd587283e1b914a22e45946c5485b", - "sha256:8bcc86b5a07ea9745f26dfad958dde0a4f56748c2ae0c9a96200a334d1b55055", - "sha256:8e2a90b8300812d8d774f2d2fc216fec3c7d94132ac589e062489c395061f16c", - "sha256:8e797e5a2a99d1b237183e52251abfc1ad85c376278b39d1aca76a451a97861a", - "sha256:948ca690f9770ad4a93fa183061c11346505598f5f0b721965bc85ec83bb103d", - "sha256:9ba5fc2655749066d68986de8368984dad4082db2fbeade78f40506dc5b65672", - "sha256:9ee553f7732fa51e019e3329a6984593184c4e0410af1e73d91ce38a5d4b34ab", - "sha256:a2cf80e5deaaa68c6cefb25303a4c870490b4e7591ed8e2435a65728920bc097", - "sha256:ae4e3f02ab5dabb35adca606237c7e1a515c86d69c0b7092bbe0e1cfe5cffc61", - "sha256:b16a2129f9146faa080bfd1b53447761f7386ec5c72890c827a65f33ab200336", - "sha256:b32addbf56639a9a8261fb62f8ea83473447671c83ca2c017ab1eabf4841157f", - "sha256:b8faff03231ee55d5a216ce3e9171c5205459f866f54d4b5ee8aa1d860e4ce11", - "sha256:bb15b2b79b00d3f6cf7d62096f5e782fa740ecedfe0540c09f1d1e4d3d7b81ba", - "sha256:bdbe785e046d124f73cca603ee37d5fae0b15dc4c13702488ad19de56aae08ba", - "sha256:bfa82614a98ecee70272bb6038d210b2ad7b2a6b8a678b400c34bdaf776802a7", - "sha256:c01dbc3671d0649023daf623e952f9f0b4d904d57ab546d6d35a4aeb14915e8d", - "sha256:c5dbc8b87cdc7be070a499f2bd1cd405c7f647abeb3447dfd397639df040bc64", - "sha256:cb51432652ad663b4cbd631c73c90f9e94f463382b86c0b6b854173700512a70", - "sha256:cc6bf01211b5ea40f633d5502c5aa495b415ebaff66e041820997dae70a508e1", - "sha256:d329578abe47ce95baa015ef3825acebb1b73b5fa6f818fdf2d4685a00ca457f", - "sha256:d4380bd6697cc7db3c9e6843f24779ac0550affa9d9a8e5f9e5d5cc139cb6583", - "sha256:d79d374e32488c76cdb06fbdd4464083aeaa715ddca3e864bac7c7760eb03729", - "sha256:eaf334477bc0a9283d5150a56be8670a07295ef676e5b5a7f086952929d1a56b", - "sha256:f6e79643368828d4651146a486be5a662846ac223ab5e2c73ddd519acfcc243c", - "sha256:f92d5d2eb119a6518755c4c9170112094c706d1c604460f50afc1308eeb97f0e", - "sha256:f97ed8bc5b517844a71030f74e9561de92f4902c306e6ccc8331a5b0c8dd0e00", - "sha256:fcdef7687aed5c4331c9808f4a414a41987441c3e7a2ba554e4dccfa4218e788", - "sha256:fd72c0b2e7443fff6e4481991742b72c17f73735e5fdd176406ca48df187a5c9", - "sha256:fe013942ab7f3241fcbe66ee43222d47f499d1e0cb69e913791c52e638ddd7f0" - ], - "index": "pypi", - "version": "==2.9.0" - }, - "tomlkit": { - "hashes": [ - "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b", - "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73" - ], - "markers": "python_version >= '3.6'", - "version": "==0.11.6" - }, - "tox": { - "hashes": [ - "sha256:3910e7ddf260de9004738a416f2efdbca21ad7f35d279f8a323117256696535f", - "sha256:aa1c07530f07265d025d534715f5e8d522606db71568cf6acbf7eadc30a8d0ed" - ], - "index": "pypi", - "version": "==4.1.2" - }, - "twine": { - "hashes": [ - "sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8", - "sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8" - ], - "index": "pypi", - "version": "==4.0.2" - }, - "typing-extensions": { - "hashes": [ - "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", - "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" - ], - "markers": "python_version >= '3.7'", - "version": "==4.4.0" - }, - "typing-inspect": { - "hashes": [ - "sha256:5fbf9c1e65d4fa01e701fe12a5bca6c6e08a4ffd5bc60bfac028253a447c5188", - "sha256:8b1ff0c400943b6145df8119c41c244ca8207f1f10c9c057aeed1560e4806e3d" - ], - "version": "==0.8.0" - }, - "urllib3": { - "hashes": [ - "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", - "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.13" - }, - "virtualenv": { - "hashes": [ - "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4", - "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058" - ], - "markers": "python_version >= '3.6'", - "version": "==20.17.1" - }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, - "wrapt": { - "hashes": [ - "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3", - "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b", - "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4", - "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2", - "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656", - "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3", - "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff", - "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310", - "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a", - "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57", - "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069", - "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383", - "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe", - "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87", - "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d", - "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b", - "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907", - "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f", - "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0", - "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28", - "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1", - "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853", - "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc", - "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3", - "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3", - "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164", - "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1", - "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c", - "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1", - "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7", - "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1", - "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320", - "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed", - "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1", - "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248", - "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c", - "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456", - "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77", - "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef", - "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1", - "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7", - "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86", - "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4", - "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d", - "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d", - "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8", - "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5", - "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471", - "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00", - "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68", - "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3", - "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d", - "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735", - "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d", - "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569", - "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7", - "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59", - "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5", - "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb", - "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b", - "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f", - "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462", - "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015", - "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af" - ], - "markers": "python_version >= '3.11'", - "version": "==1.14.1" - }, - "zipp": { - "hashes": [ - "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa", - "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766" - ], - "markers": "python_version >= '3.7'", - "version": "==3.11.0" - } - } -} diff --git a/README.md b/README.md index 348ccd7c..6b1fdad9 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,14 @@ pyatmo [![PyPi](https://img.shields.io/pypi/v/pyatmo.svg)](https://pypi.python.org/pypi/pyatmo) [![license](https://img.shields.io/pypi/l/pyatmo.svg)](https://github.com/jabesq/pyatmo/blob/master/LICENSE.txt) +> **Warning:** +> Due to personal reasons, I am currently unable to dedicate sufficient time to effectively manage this repository. Consequently, no attention will be given to existing or forthcoming issues until further notice. **However**, I want to assure you that I will continue to merge any pull requests that are submitted, provided they successfully pass the continuous integration tests and do not exhibit any glaring issues. +> +> I apologize for any inconvenience this may cause, and I sincerely hope to have the capacity to allocate more time to this repository in the near future. Your understanding is greatly appreciated. + +*** + + Simple API to access Netatmo devices and data like weather station or camera data from Python 3. For more detailed information see [dev.netatmo.com](http://dev.netatmo.com) @@ -64,4 +72,4 @@ Another way to run the tests is by using `tox`. This runs the tests against the or by specifying a python version - tox -e py38 + tox -e py310 diff --git a/pyproject.toml b/pyproject.toml index df66b448..6d6e94fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,128 @@ requires = ["wheel", "setuptools", "attrs>=17.1"] build-backend = "setuptools.build_meta" [tool.pytest.ini_options] -minversion = "6.0" +minversion = "7.0" asyncio_mode = "auto" +[tool.ruff] +select = [ + "B002", # Python does not support the unary prefix increment + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "B023", # Function definition does not bind loop variable {name} + "B026", # Star-arg unpacking after a keyword argument is strongly discouraged + "C", # complexity + "COM818", # Trailing comma on bare tuple prohibited + "D", # docstrings + "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() + "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) + "E", # pycodestyle + "F", # pyflakes/autoflake + "G", # flake8-logging-format + "I", # isort + "ICN001", # import concentions; {name} should be imported as {asname} + "ISC001", # Implicitly concatenated string literals on one line + "N804", # First argument of a class method should be named cls + "N805", # First argument of a method should be named self + "N815", # Variable {name} in class scope should not be mixedCase + "PGH001", # No builtin eval() allowed + "PGH004", # Use specific rule codes when using noqa + "PLC0414", # Useless import alias. Import alias does not rename original package. + "PLC", # pylint + "PLE", # pylint + "PLR", # pylint + "PLW", # pylint + "Q000", # Double quotes found but single quotes preferred + "RUF006", # Store a reference to the return value of asyncio.create_task + "S102", # Use of exec detected + "S103", # bad-file-permissions + "S108", # hardcoded-temp-file + "S306", # suspicious-mktemp-usage + "S307", # suspicious-eval-usage + "S313", # suspicious-xmlc-element-tree-usage + "S314", # suspicious-xml-element-tree-usage + "S315", # suspicious-xml-expat-reader-usage + "S316", # suspicious-xml-expat-builder-usage + "S317", # suspicious-xml-sax-usage + "S318", # suspicious-xml-mini-dom-usage + "S319", # suspicious-xml-pull-dom-usage + "S320", # suspicious-xmle-tree-usage + "S601", # paramiko-call + "S602", # subprocess-popen-with-shell-equals-true + "S604", # call-with-shell-equals-true + "S608", # hardcoded-sql-expression + "S609", # unix-command-wildcard-injection + "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass + "SIM117", # Merge with-statements that use the same scope + "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() + "SIM201", # Use {left} != {right} instead of not {left} == {right} + "SIM208", # Use {expr} instead of not (not {expr}) + "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} + "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. + "SIM401", # Use get from dict with default instead of an if block + "T100", # Trace found: {name} used + "T20", # flake8-print + "TID251", # Banned imports + "TRY004", # Prefer TypeError exception for invalid type + "TRY200", # Use raise from to specify exception cause + "TRY302", # Remove exception handler; error is immediately re-raised + "UP", # pyupgrade + "W", # pycodestyle +] + +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D406", # Section name should end with a newline + "D407", # Section name underlining + "E501", # line too long + "E731", # do not assign a lambda expression, use a def + # False positives https://github.com/astral-sh/ruff/issues/5386 + "PLC0208", # Use a sequence type instead of a `set` when iterating over values + "PLR0911", # Too many return statements ({returns} > {max_returns}) + "PLR0912", # Too many branches ({branches} > {max_branches}) + "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) + "PLR0915", # Too many statements ({statements} > {max_statements}) + "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable + "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target + "UP006", # keep type annotation style as is + "UP007", # keep type annotation style as is + # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 + "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` +] + +[tool.ruff.flake8-import-conventions.extend-aliases] +voluptuous = "vol" +"homeassistant.helpers.area_registry" = "ar" +"homeassistant.helpers.config_validation" = "cv" +"homeassistant.helpers.device_registry" = "dr" +"homeassistant.helpers.entity_registry" = "er" +"homeassistant.helpers.issue_registry" = "ir" +"homeassistant.util.dt" = "dt_util" + +[tool.ruff.flake8-pytest-style] +fixture-parentheses = false + +[tool.ruff.flake8-tidy-imports.banned-api] +"pytz".msg = "use zoneinfo instead" + +[tool.ruff.isort] +force-sort-within-sections = true +known-first-party = ["homeassistant"] +combine-as-imports = true +split-on-trailing-comma = false + +[tool.ruff.per-file-ignores] +# Allow for main entry & scripts to write to stdout +"src/pyatmo/__main__.py" = ["T201"] + +# Exceptions for tests +"tests/*" = ["D10"] + +[tool.ruff.mccabe] +max-complexity = 25 + [tool.setuptools_scm] local_scheme = "no-local-version" tag_regex = "^(?Pv)?(?P[^\\+]+)(?P.*)?$" diff --git a/setup.cfg b/setup.cfg index 171b16be..25b7ef0f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,16 +7,14 @@ url = https://github.com/jabesq/pyatmo author = Hugo Dupras author_email = jabesq@gmail.com license = MIT -license_file = LICENSE.txt license_files = LICENSE.txt classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Home Automation @@ -28,7 +26,7 @@ install_requires = oauthlib~=3.1 requests~=2.24 requests-oauthlib~=1.3 -python_requires = >=3.8 +python_requires = >=3.10 include_package_data = True package_dir = =src setup_requires = diff --git a/src/pyatmo/__main__.py b/src/pyatmo/__main__.py index 6dd99dcb..a3966cfd 100644 --- a/src/pyatmo/__main__.py +++ b/src/pyatmo/__main__.py @@ -1,3 +1,4 @@ +"""Main entry point for pyatmo CLI.""" import os import sys from warnings import warn @@ -21,6 +22,7 @@ def tty_print(message: str) -> None: """Print to stdout if in an interactive terminal.""" + if sys.stdout.isatty(): print(message) diff --git a/src/pyatmo/account.py b/src/pyatmo/account.py index 2e187b32..9ef29f04 100644 --- a/src/pyatmo/account.py +++ b/src/pyatmo/account.py @@ -31,11 +31,8 @@ class AsyncAccount: """Async class of a Netatmo account.""" def __init__(self, auth: AbstractAsyncAuth, favorite_stations: bool = True) -> None: - """Initialize the Netatmo account. + """Initialize the Netatmo account.""" - Arguments: - auth {AbstractAsyncAuth} -- Authentication information with valid access token - """ self.auth: AbstractAsyncAuth = auth self.user: str | None = None self.homes: dict[str, Home] = {} @@ -45,12 +42,15 @@ def __init__(self, auth: AbstractAsyncAuth, favorite_stations: bool = True) -> N self.modules: dict[str, Module] = {} def __repr__(self) -> str: + """Return the representation.""" + return ( f"{self.__class__.__name__}(user={self.user}, home_ids={self.homes.keys()}" ) def process_topology(self) -> None: """Process topology information from /homesdata.""" + for home in self.raw_data["homes"]: if (home_id := home["id"]) in self.homes: self.homes[home_id].update_topology(home) @@ -59,6 +59,7 @@ def process_topology(self) -> None: async def async_update_topology(self) -> None: """Retrieve topology data from /homesdata.""" + resp = await self.auth.async_post_api_request( endpoint=GETHOMESDATA_ENDPOINT, ) @@ -106,6 +107,8 @@ async def async_update_measures( interval: MeasureInterval = MeasureInterval.HOUR, days: int = 7, ) -> None: + """Retrieve measures data from /getmeasure.""" + await getattr(self.homes[home_id].modules[module_id], "async_update_measures")( start_time=start_time, interval=interval, @@ -124,6 +127,7 @@ def register_public_weather_area( area_id: str = str(uuid4()), ) -> str: """Register public weather area to monitor.""" + self.public_weather_areas[area_id] = modules.PublicWeatherArea( lat_ne, lon_ne, @@ -135,7 +139,7 @@ def register_public_weather_area( return area_id async def async_update_public_weather(self, area_id: str) -> None: - """Retrieve status data from /getpublicdata""" + """Retrieve status data from /getpublicdata.""" params = { "lat_ne": self.public_weather_areas[area_id].location.lat_ne, "lon_ne": self.public_weather_areas[area_id].location.lon_ne, diff --git a/src/pyatmo/auth.py b/src/pyatmo/auth.py index a73ea16e..8d20840c 100644 --- a/src/pyatmo/auth.py +++ b/src/pyatmo/auth.py @@ -1,16 +1,17 @@ """Support for Netatmo authentication.""" from __future__ import annotations -import asyncio -import logging from abc import ABC, abstractmethod +import asyncio +from collections.abc import Callable from json import JSONDecodeError +import logging from time import sleep -from typing import Any, Callable +from typing import Any -import requests from aiohttp import ClientError, ClientResponse, ClientSession, ContentTypeError from oauthlib.oauth2 import LegacyApplicationClient, TokenExpiredError +import requests from requests_oauthlib import OAuth2Session from pyatmo.const import ( @@ -29,9 +30,7 @@ class NetatmoOAuth2: - """ - Handle authentication with OAuth2 - """ + """Handle authentication with OAuth2.""" def __init__( self, @@ -44,29 +43,29 @@ def __init__( user_prefix: str | None = None, base_url: str = DEFAULT_BASE_URL, ) -> None: - """Initialize self. - - Keyword Arguments: - client_id {str} -- Application client ID delivered by Netatmo on dev.netatmo.com (default: {None}) - client_secret {str} -- Application client secret delivered by Netatmo on dev.netatmo.com (default: {None}) - redirect_uri {Optional[str]} -- Redirect URI where to the authorization server will redirect with an authorization code (default: {None}) - token {Optional[Dict[str, str]]} -- Authorization token (default: {None}) - token_updater {Optional[Callable[[str], None]]} -- Callback when the token is updated (default: {None}) - scope {Optional[str]} -- List of scopes (default: {"read_station"}) - read_station: to retrieve weather station data (Getstationsdata, Getmeasure) - read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) - access_camera: to access the camera, the videos and the live stream - write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) - read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) - write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) - read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) - access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status - read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) - read_smokedetector: to retrieve the smoke detector status (Gethomedata) - Several values can be used at the same time, ie: 'read_station read_camera' - user_prefix {Optional[str]} -- API prefix for the Netatmo customer - base_url {str} -- Base URL of the Netatmo API (default: {_DEFAULT_BASE_URL}) - """ + """Initialize self.""" + + # Keyword Arguments: + # client_id {str} -- Application client ID delivered by Netatmo on dev.netatmo.com (default: {None}) + # client_secret {str} -- Application client secret delivered by Netatmo on dev.netatmo.com (default: {None}) + # redirect_uri {Optional[str]} -- Redirect URI where to the authorization server will redirect with an authorization code (default: {None}) + # token {Optional[Dict[str, str]]} -- Authorization token (default: {None}) + # token_updater {Optional[Callable[[str], None]]} -- Callback when the token is updated (default: {None}) + # scope {Optional[str]} -- List of scopes (default: {"read_station"}) + # read_station: to retrieve weather station data (Getstationsdata, Getmeasure) + # read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) + # access_camera: to access the camera, the videos and the live stream + # write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) + # read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) + # write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) + # read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) + # access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status + # read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) + # read_smokedetector: to retrieve the smoke detector status (Gethomedata) + # Several values can be used at the same time, ie: 'read_station read_camera' + # user_prefix {Optional[str]} -- API prefix for the Netatmo customer + # base_url {str} -- Base URL of the Netatmo API (default: {_DEFAULT_BASE_URL}) + self.client_id = client_id self.client_secret = client_secret self.redirect_uri = redirect_uri @@ -92,6 +91,7 @@ def __init__( def refresh_tokens(self) -> Any: """Refresh and return new tokens.""" + token = self._oauth.refresh_token( self.base_url + AUTH_REQ_ENDPOINT, **self.extra, @@ -108,6 +108,8 @@ def post_api_request( params: dict[str, Any] | None = None, timeout: int = 5, ) -> requests.Response: + """Wrap post requests.""" + return self.post_request( url=self.base_url + endpoint, params=params, @@ -120,7 +122,8 @@ def post_request( params: dict[str, Any] | None = None, timeout: int = 5, ) -> requests.Response: - """Wrapper for post requests.""" + """Wrap post requests.""" + resp = requests.Response() req_args = {"data": params if params is not None else {}} @@ -201,6 +204,8 @@ def query( return requests.Response() def get_authorization_url(self, state: str | None = None) -> Any: + """Return the authorization URL.""" + return self._oauth.authorization_url(self.base_url + AUTH_URL_ENDPOINT, state) def request_token( @@ -208,12 +213,8 @@ def request_token( authorization_response: str | None = None, code: str | None = None, ) -> Any: - """ - Generic method for fetching a Netatmo access token. - :param authorization_response: Authorization response URL, the callback URL of the request back to you. - :param code: Authorization code. - :return: A token dict. - """ + """Request token.""" + return self._oauth.fetch_token( self.base_url + AUTH_REQ_ENDPOINT, authorization_response=authorization_response, @@ -224,39 +225,43 @@ def request_token( ) def addwebhook(self, webhook_url: str) -> None: + """Register webhook.""" + post_params = {"url": webhook_url} resp = self.post_api_request(WEBHOOK_URL_ADD_ENDPOINT, post_params) LOG.debug("addwebhook: %s", resp) def dropwebhook(self) -> None: + """Unregister webhook.""" + post_params = {"app_types": "app_security"} resp = self.post_api_request(WEBHOOK_URL_DROP_ENDPOINT, post_params) LOG.debug("dropwebhook: %s", resp) class ClientAuth(NetatmoOAuth2): - """ - Request authentication and keep access token available through token method. Renew it automatically if necessary - Args: - clientId (str): Application clientId delivered by Netatmo on dev.netatmo.com - clientSecret (str): Application Secret key delivered by Netatmo on dev.netatmo.com - username (str) - password (str) - scope (Optional[str]): - read_station: to retrieve weather station data (Getstationsdata, Getmeasure) - read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) - access_camera: to access the camera, the videos and the live stream - write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) - read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) - write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) - read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) - access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status - read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) - read_smokedetector: to retrieve the smoke detector status (Gethomedata) - Several value can be used at the same time, ie: 'read_station read_camera' - user_prefix (Optional[str]) -- API prefix for the Netatmo customer - base_url (str) -- Base URL of the Netatmo API (default: {_DEFAULT_BASE_URL}) - """ + """Request authentication and keep access token available through token method.""" + + # Renew it automatically if necessary + # Args: + # clientId (str): Application clientId delivered by Netatmo on dev.netatmo.com + # clientSecret (str): Application Secret key delivered by Netatmo on dev.netatmo.com + # username (str) + # password (str) + # scope (Optional[str]): + # read_station: to retrieve weather station data (Getstationsdata, Getmeasure) + # read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) + # access_camera: to access the camera, the videos and the live stream + # write_camera: to set home/away status of persons (Setpersonsaway, Setpersonshome) + # read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) + # write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) + # read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) + # access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status + # read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) + # read_smokedetector: to retrieve the smoke detector status (Gethomedata) + # Several value can be used at the same time, ie: 'read_station read_camera' + # user_prefix (Optional[str]) -- API prefix for the Netatmo customer + # base_url (str) -- Base URL of the Netatmo API (default: {_DEFAULT_BASE_URL}). def __init__( self, @@ -267,7 +272,9 @@ def __init__( scope: str = "read_station", user_prefix: str | None = None, base_url: str = DEFAULT_BASE_URL, - ): + ) -> None: + """Initialize self.""" + super().__init__( client_id=client_id, client_secret=client_secret, @@ -299,6 +306,7 @@ def __init__( base_url: str = DEFAULT_BASE_URL, ) -> None: """Initialize the auth.""" + self.websession = websession self.base_url = base_url @@ -313,7 +321,8 @@ async def async_get_image( params: dict[str, Any] | None = None, timeout: int = 5, ) -> bytes: - """Wrapper for async get requests.""" + """Wrap async get requests.""" + try: access_token = await self.async_get_access_token() except ClientError as err: @@ -348,6 +357,8 @@ async def async_post_api_request( params: dict[str, Any] | None = None, timeout: int = 5, ) -> ClientResponse: + """Wrap async post requests.""" + return await self.async_post_request( url=(base_url or self.base_url) + endpoint, params=params, @@ -360,7 +371,8 @@ async def async_post_request( params: dict[str, Any] | None = None, timeout: int = 5, ) -> ClientResponse: - """Wrapper for async post requests.""" + """Wrap async post requests.""" + try: access_token = await self.async_get_access_token() except ClientError as err: @@ -369,6 +381,10 @@ async def async_post_request( req_args = {"data": params if params is not None else {}} + if "params" in req_args["data"]: + req_args["params"] = req_args["data"]["params"] + req_args["data"].pop("params") + if "json" in req_args["data"]: req_args["json"] = req_args["data"]["json"] req_args.pop("data") diff --git a/src/pyatmo/camera.py b/src/pyatmo/camera.py index e1b24b16..7b1f5d5d 100644 --- a/src/pyatmo/camera.py +++ b/src/pyatmo/camera.py @@ -1,10 +1,10 @@ """Support for Netatmo security devices (cameras, smoke detectors, sirens, window sensors, events and persons).""" from __future__ import annotations -import imghdr -import time from abc import ABC from collections import defaultdict +import imghdr # pylint: disable=deprecated-module +import time from typing import Any from warnings import warn @@ -138,10 +138,8 @@ def get_smokedetector(self, smoke_id: str) -> dict | None: ) def camera_urls(self, camera_id: str) -> tuple[str | None, str | None]: - """ - Return the vpn_url and the local_url (if available) of a given camera - in order to access its live feed. - """ + """Return the vpn_url and the local_url (if available) of a given camera.""" + camera_data = self.get_camera(camera_id) return camera_data.get("vpn_url", None), camera_data.get("local_url", None) @@ -367,6 +365,7 @@ def module_motion_detected( exclude: int = 0, ) -> bool: """Evaluate if movement has been detected.""" + if exclude: limit = time.time() - exclude array_time_event = sorted(self.events.get(camera_id, []), reverse=True) @@ -397,6 +396,7 @@ def module_motion_detected( def module_opened(self, module_id: str, camera_id: str, exclude: int = 0) -> bool: """Evaluate if module status is open.""" + if exclude: limit = time.time() - exclude array_time_event = sorted(self.events.get(camera_id, []), reverse=True) @@ -433,6 +433,7 @@ def build_state_params( monitoring: str | None, ): """Build camera state parameters.""" + if home_id is None: home_id = self.get_camera(camera_id)["home_id"] @@ -459,11 +460,8 @@ class CameraData(AbstractCameraData): """Class of Netatmo camera data.""" def __init__(self, auth: NetatmoOAuth2) -> None: - """Initialize the Netatmo camera data. + """Initialize the Netatmo camera data.""" - Arguments: - auth {NetatmoOAuth2} -- Authentication information with valid access token - """ self.auth = auth def update(self, events: int = 30) -> None: @@ -480,12 +478,14 @@ def update(self, events: int = 30) -> None: def _update_all_camera_urls(self) -> None: """Update all camera urls.""" + for home_id in self.homes: for camera_id in self.cameras[home_id]: self.update_camera_urls(camera_id) def update_camera_urls(self, camera_id: str) -> None: """Update and validate the camera urls.""" + camera_data = self.get_camera(camera_id) home_id = camera_data["home_id"] @@ -507,6 +507,8 @@ def update_camera_urls(self, camera_id: str) -> None: self.cameras[home_id][camera_id]["is_local"] = False def _check_url(self, url: str) -> str | None: + """Check if the url is valid.""" + if url.startswith("http://169.254"): return None resp_json = {} @@ -532,17 +534,17 @@ def set_state( floodlight: str | None = None, monitoring: str | None = None, ) -> bool: - """Turn camera (light) on/off. + """Turn camera (light) on/off.""" - Arguments: - camera_id {str} -- ID of a camera - home_id {str} -- ID of a home - floodlight {str} -- Mode for floodlight (on/off/auto) - monitoring {str} -- Mode for monitoring (on/off) + # Arguments: + # camera_id {str} -- ID of a camera + # home_id {str} -- ID of a home + # floodlight {str} -- Mode for floodlight (on/off/auto) + # monitoring {str} -- Mode for monitoring (on/off) + + # Returns: + # Boolean -- Success of the request - Returns: - Boolean -- Success of the request - """ post_params = { "json": { "home": self.build_state_params( @@ -657,15 +659,13 @@ class AsyncCameraData(AbstractCameraData): """Class of Netatmo camera data.""" def __init__(self, auth: AbstractAsyncAuth) -> None: - """Initialize the Netatmo camera data. + """Initialize the Netatmo camera data.""" - Arguments: - auth {AbstractAsyncAuth} -- Authentication information with valid access token - """ self.auth = auth async def async_update(self, events: int = 30) -> None: """Fetch and process data from API.""" + resp = await self.auth.async_post_api_request( endpoint=GETHOMEDATA_ENDPOINT, params={"size": events}, @@ -684,6 +684,7 @@ async def async_update(self, events: int = 30) -> None: async def _async_update_all_camera_urls(self) -> None: """Update all camera urls.""" + for home_id in self.homes: for camera_id in self.cameras[home_id]: await self.async_update_camera_urls(camera_id) @@ -695,17 +696,14 @@ async def async_set_state( floodlight: str | None = None, monitoring: str | None = None, ) -> bool: - """Turn camera (light) on/off. + """Turn camera (light) on/off.""" - Arguments: - camera_id {str} -- ID of a camera - home_id {str} -- ID of a home - floodlight {str} -- Mode for floodlight (on/off/auto) - monitoring {str} -- Mode for monitoring (on/off) + # Arguments: + # camera_id {str} -- ID of a camera + # home_id {str} -- ID of a home + # floodlight {str} -- Mode for floodlight (on/off/auto) + # monitoring {str} -- Mode for monitoring (on/off) - Returns: - Boolean -- Success of the request - """ post_params = { "json": { "home": self.build_state_params( diff --git a/src/pyatmo/const.py b/src/pyatmo/const.py index 0753a4be..58538f76 100644 --- a/src/pyatmo/const.py +++ b/src/pyatmo/const.py @@ -1,7 +1,7 @@ """Common constants.""" from __future__ import annotations -from typing import Any, Dict +from typing import Any ERRORS: dict[int, str] = { 400: "Bad request", @@ -15,7 +15,7 @@ } # Special types -RawData = Dict[str, Any] +RawData = dict[str, Any] DEFAULT_BASE_URL: str = "https://api.netatmo.com/" @@ -32,6 +32,7 @@ SETROOMTHERMPOINT_ENDPOINT = "api/setroomthermpoint" GETROOMMEASURE_ENDPOINT = "api/getroommeasure" SWITCHHOMESCHEDULE_ENDPOINT = "api/switchhomeschedule" +SYNCHOMESCHEDULE_ENDPOINT = "api/synchomeschedule" GETHOMEDATA_ENDPOINT = "api/gethomedata" GETCAMERAPICTURE_ENDPOINT = "api/getcamerapicture" @@ -50,7 +51,7 @@ AUTHORIZATION_HEADER = "Authorization" # Possible scops -ALL_SCOPES = [ +ALL_SCOPES: list[str] = [ "access_camera", # Netatmo camera products "access_doorbell", # Netatmo Smart Video Doorbell "access_presence", # Netatmo Smart Outdoor Camera @@ -66,6 +67,7 @@ "read_smokedetector", # Smart Smoke Alarm information and events "read_station", # Netatmo weather station "read_thermostat", # Netatmo climate products + "read_mhs1", # Bticino MyHome Server 1 modules "write_bubendorff", # Bubbendorf shutters "write_camera", # Netatmo camera products "write_magellan", # Legrand Wiring device or Electrical panel products @@ -73,6 +75,7 @@ "write_presence", # Netatmo Smart Outdoor Camera "write_smarther", # Smarther products "write_thermostat", # Netatmo climate products + "write_mhs1", # Bticino MyHome Server 1 modules ] MANUAL = "manual" diff --git a/src/pyatmo/event.py b/src/pyatmo/event.py index 5a9d80c1..7c9c201b 100644 --- a/src/pyatmo/event.py +++ b/src/pyatmo/event.py @@ -95,10 +95,14 @@ class Event: subevents: list[Event] | None = None def __init__(self, home_id: str, raw_data: RawData) -> None: + """Initialize a Netatmo event instance.""" + self.home_id = home_id self._init_attributes(raw_data) def _init_attributes(self, raw_data: RawData) -> None: + """Initialize attributes of the instance.""" + for attrib, value in raw_data.items(): if attrib == "subevents": value = [Event(self.home_id, event) for event in value] diff --git a/src/pyatmo/exceptions.py b/src/pyatmo/exceptions.py index 2867f4e2..4ed5f120 100644 --- a/src/pyatmo/exceptions.py +++ b/src/pyatmo/exceptions.py @@ -1,25 +1,43 @@ -"""Exceptions.""" +"""Exceptions for pyatmo.""" class NoSchedule(Exception): + """Raised when no schedule is found.""" + + pass + + +class InvalidSchedule(Exception): + """Raised when an invalid schedule is encountered.""" + pass class InvalidHome(Exception): + """Raised when an invalid home is encountered.""" + pass class InvalidRoom(Exception): + """Raised when an invalid room is encountered.""" + pass class NoDevice(Exception): + """Raised when no device is found.""" + pass class ApiError(Exception): + """Raised when an API error is encountered.""" + pass class InvalidState(Exception): + """Raised when an invalid state is encountered.""" + pass diff --git a/src/pyatmo/helpers.py b/src/pyatmo/helpers.py index 89412aaa..d905eab2 100644 --- a/src/pyatmo/helpers.py +++ b/src/pyatmo/helpers.py @@ -1,10 +1,10 @@ """Collection of helper functions.""" from __future__ import annotations +from calendar import timegm +from datetime import datetime, timezone import logging import time -from calendar import timegm -from datetime import datetime from typing import Any from pyatmo.const import RawData @@ -14,20 +14,31 @@ def to_time_string(value: str) -> str: - return datetime.utcfromtimestamp(int(value)).isoformat(sep="_") + """Convert epoch to time string.""" + + return ( + datetime.fromtimestamp(int(value), tz=timezone.utc) + .isoformat(sep="_") + .split("+")[0] + ) def to_epoch(value: str) -> int: + """Convert time string to epoch.""" + return timegm(time.strptime(f"{value}GMT", "%Y-%m-%d_%H:%M:%S%Z")) def today_stamps() -> tuple[int, int]: + """Return today's start and end timestamps.""" + today: int = timegm(time.strptime(time.strftime("%Y-%m-%d") + "GMT", "%Y-%m-%d%Z")) return today, today + 3600 * 24 def fix_id(raw_data: RawData) -> dict[str, Any]: """Fix known errors in station ids like superfluous spaces.""" + if not raw_data: return raw_data diff --git a/src/pyatmo/home.py b/src/pyatmo/home.py index 06a9d8ea..ac6dd2cd 100644 --- a/src/pyatmo/home.py +++ b/src/pyatmo/home.py @@ -15,10 +15,11 @@ SETSTATE_ENDPOINT, SETTHERMMODE_ENDPOINT, SWITCHHOMESCHEDULE_ENDPOINT, + SYNCHOMESCHEDULE_ENDPOINT, RawData, ) from pyatmo.event import Event -from pyatmo.exceptions import InvalidState, NoSchedule +from pyatmo.exceptions import InvalidSchedule, InvalidState, NoSchedule from pyatmo.modules import Module from pyatmo.person import Person from pyatmo.room import Room @@ -43,6 +44,8 @@ class Home: events: dict[str, Event] def __init__(self, auth: AbstractAsyncAuth, raw_data: RawData) -> None: + """Initialize a Netatmo home instance.""" + self.auth = auth self.entity_id = raw_data["id"] self.name = raw_data.get("name", "Unknown") @@ -68,6 +71,8 @@ def __init__(self, auth: AbstractAsyncAuth, raw_data: RawData) -> None: self.events = {} def get_module(self, module) -> Module: + """Return module.""" + try: return getattr(modules, module["type"])( home=self, @@ -81,6 +86,8 @@ def get_module(self, module) -> Module: ) def update_topology(self, raw_data: RawData) -> None: + """Update topology.""" + self.name = raw_data.get("name", "Unknown") raw_modules = raw_data.get("modules", []) @@ -118,6 +125,8 @@ def update_topology(self, raw_data: RawData) -> None: } async def update(self, raw_data: RawData) -> None: + """Update home with the latest data.""" + for module in raw_data.get("errors", []): await self.modules[module["id"]].update({}) @@ -149,6 +158,7 @@ async def update(self, raw_data: RawData) -> None: def get_selected_schedule(self) -> Schedule | None: """Return selected schedule for given home.""" + return next( (schedule for schedule in self.schedules.values() if schedule.selected), None, @@ -156,19 +166,24 @@ def get_selected_schedule(self) -> Schedule | None: def is_valid_schedule(self, schedule_id: str) -> bool: """Check if valid schedule.""" + return schedule_id in self.schedules def has_otm(self) -> bool: + """Check if any room has an OTM device.""" + return any("OTM" in room.device_types for room in self.rooms.values()) def get_hg_temp(self) -> float | None: """Return frost guard temperature value for given home.""" + if (schedule := self.get_selected_schedule()) is None: return None return schedule.hg_temp def get_away_temp(self) -> float | None: """Return configured away temperature value for given home.""" + if (schedule := self.get_selected_schedule()) is None: return None return schedule.away_temp @@ -245,6 +260,7 @@ async def async_set_persons_away( person_id: str | None = None, ) -> ClientResponse: """Mark a person as away or set the whole home to being empty.""" + post_params = {"home_id": self.entity_id} if person_id: post_params["person_id"] = person_id @@ -253,7 +269,91 @@ async def async_set_persons_away( params=post_params, ) + async def async_set_schedule_temperatures( + self, + zone_id: int, + temps: dict[str, int], + ) -> None: + """Set the scheduled room temperature for the given schedule ID.""" + + selected_schedule = self.get_selected_schedule() + + if selected_schedule is None: + raise NoSchedule("Could not determine selected schedule.") + + zones = [] + + timetable_entries = [ + { + "m_offset": timetable_entry.m_offset, + "zone_id": timetable_entry.zone_id, + } + for timetable_entry in selected_schedule.timetable + ] + + for zone in selected_schedule.zones: + new_zone = { + "id": zone.entity_id, + "name": zone.name, + "type": zone.type, + "rooms": [], + } + + for room in zone.rooms: + temp = room.therm_setpoint_temperature + if zone.entity_id == zone_id and room.entity_id in temps: + temp = temps[room.entity_id] + + new_zone["rooms"].append( + {"id": room.entity_id, "therm_setpoint_temperature": temp}, + ) + + zones.append(new_zone) + + schedule = { + "away_temp": selected_schedule.away_temp, + "hg_temp": selected_schedule.hg_temp, + "timetable": timetable_entries, + "zones": zones, + } + + await self.async_sync_schedule(selected_schedule.entity_id, schedule) + + async def async_sync_schedule( + self, + schedule_id: str, + schedule: dict[str, Any], + ) -> None: + """Modify an existing schedule.""" + if not is_valid_schedule(schedule): + raise InvalidSchedule("Data for '/synchomeschedule' contains errors.") + LOG.debug( + "Setting schedule (%s) for home (%s) to %s", + schedule_id, + self.entity_id, + schedule, + ) + + resp = await self.auth.async_post_api_request( + endpoint=SYNCHOMESCHEDULE_ENDPOINT, + params={ + "params": { + "home_id": self.entity_id, + "schedule_id": schedule_id, + "name": "Default", + }, + "json": schedule, + }, + ) + + return (await resp.json()).get("status") == "ok" + def is_valid_state(data: dict[str, Any]) -> bool: """Check set state data.""" return data is not None + + +def is_valid_schedule(schedule: dict[str, Any]) -> bool: + """Check schedule.""" + return schedule is not None diff --git a/src/pyatmo/home_coach.py b/src/pyatmo/home_coach.py index ba48f335..020dc92d 100644 --- a/src/pyatmo/home_coach.py +++ b/src/pyatmo/home_coach.py @@ -9,28 +9,16 @@ class HomeCoachData(WeatherStationData): - """ - Class of Netatmo Home Coach devices (stations and modules) - """ + """Class of Netatmo Home Coach devices (stations and modules).""" def __init__(self, auth: NetatmoOAuth2) -> None: - """Initialize self. - - Arguments: - auth {NetatmoOAuth2} -- Authentication information with valid access token - """ + """Initialize self.""" super().__init__(auth, endpoint=GETHOMECOACHDATA_ENDPOINT, favorites=False) class AsyncHomeCoachData(AsyncWeatherStationData): - """ - Class of Netatmo Home Coach devices (stations and modules) - """ + """Class of Netatmo Home Coach devices (stations and modules).""" def __init__(self, auth: AbstractAsyncAuth) -> None: - """Initialize self. - - Arguments: - auth {AbstractAsyncAuth} -- Authentication information with valid access token - """ + """Initialize self.""" super().__init__(auth, endpoint=GETHOMECOACHDATA_ENDPOINT, favorites=False) diff --git a/src/pyatmo/modules/__init__.py b/src/pyatmo/modules/__init__.py index 882aae59..ffa4898c 100644 --- a/src/pyatmo/modules/__init__.py +++ b/src/pyatmo/modules/__init__.py @@ -1,6 +1,21 @@ """Expose submodules.""" from .base_class import Place -from .bticino import BNCX, BNDL, BNEU, BNSL +from .bticino import ( + BNAB, + BNAS, + BNCS, + BNCX, + BNDL, + BNEU, + BNFC, + BNIL, + BNMH, + BNMS, + BNSL, + BNTH, + BNTR, + BNXM, +) from .idiamant import NBG, NBO, NBR, NBS from .legrand import ( EBU, @@ -15,6 +30,7 @@ NLFN, NLG, NLIS, + NLJ, NLL, NLLF, NLLM, @@ -23,6 +39,7 @@ NLP, NLPBS, NLPC, + NLPD, NLPM, NLPO, NLPS, @@ -64,11 +81,21 @@ from .somfy import TPSRS __all__ = [ + "BNMS", + "BNAS", + "BNAB", + "BNMH", + "BNTH", + "BNFC", + "BNTR", + "BNXM", + "BNCS", "BNCX", "BNDL", "BNEU", "BNS", "BNSL", + "BNIL", "Camera", "Dimmer", "Location", @@ -106,6 +133,7 @@ "NLP", "NLPBS", "NLPC", + "NLPD", "NLPM", "NLPO", "NLPS", @@ -133,4 +161,5 @@ "TPSRS", "NLAS", "NLTS", + "NLJ", ] diff --git a/src/pyatmo/modules/base_class.py b/src/pyatmo/modules/base_class.py index 511744ae..3e6dec0c 100644 --- a/src/pyatmo/modules/base_class.py +++ b/src/pyatmo/modules/base_class.py @@ -1,10 +1,11 @@ """Base class for Netatmo entities.""" from __future__ import annotations -import logging from abc import ABC +from collections.abc import Iterable from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Iterable +import logging +from typing import TYPE_CHECKING, Any from pyatmo.const import RawData from pyatmo.modules.device_types import DeviceType @@ -29,10 +30,14 @@ def default(key: str, val: Any) -> Any: + """Return default value.""" + return lambda x, _: x.get(key, val) class EntityBase: + """Base class for Netatmo entities.""" + entity_id: str home: Home bridge: str | None @@ -42,10 +47,14 @@ class NetatmoBase(EntityBase, ABC): """Base class for Netatmo entities.""" def __init__(self, raw_data: RawData) -> None: + """Initialize a Netatmo entity.""" + self.entity_id = raw_data["id"] self.name = raw_data.get("name", f"Unknown {self.entity_id}") def update_topology(self, raw_data: RawData) -> None: + """Update topology.""" + self._update_attributes(raw_data) if ( @@ -56,6 +65,8 @@ def update_topology(self, raw_data: RawData) -> None: self.name = f"{self.home.modules[self.bridge].name} {self.name}" def _update_attributes(self, raw_data: RawData) -> None: + """Update attributes.""" + self.__dict__ = { key: NETATMO_ATTRIBUTES_MAP.get(key, default(key, val))(raw_data, val) for key, val in self.__dict__.items() @@ -64,20 +75,28 @@ def _update_attributes(self, raw_data: RawData) -> None: @dataclass class Location: + """Class of Netatmo public weather location.""" + latitude: float longitude: float def __init__(self, longitude: float, latitude: float) -> None: + """Initialize self.""" + self.latitude = latitude self.longitude = longitude def __iter__(self) -> Iterable[float]: + """Iterate over latitude and longitude.""" + yield self.longitude yield self.latitude @dataclass class Place: + """Class of Netatmo public weather place.""" + altitude: int | None city: str | None country: str | None @@ -88,6 +107,8 @@ def __init__( self, data: dict[str, Any], ) -> None: + """Initialize self.""" + if data is None: return self.altitude = data.get("altitude") diff --git a/src/pyatmo/modules/bticino.py b/src/pyatmo/modules/bticino.py index 910eda9a..33391a52 100644 --- a/src/pyatmo/modules/bticino.py +++ b/src/pyatmo/modules/bticino.py @@ -22,3 +22,43 @@ class BNCX(Module): class BNEU(Module): """BTicino external unit.""" + + +class BNCS(Module): + """BTicino camera.""" + + +class BNXM(Module): + """BTicino X meter.""" + + +class BNMS(Module): + """BTicino motorized shade.""" + + +class BNAS(Module): + """BTicino automatic shutter.""" + + +class BNAB(Module): + """BTicino automatic blind.""" + + +class BNMH(Module): + """BTicino automatic blind.""" + + +class BNTH(Module): + """BTicino thermostat.""" + + +class BNFC(Module): + """BTicino fan coil.""" + + +class BNTR(Module): + """BTicino radiator thermostat.""" + + +class BNIL(Switch): + """BTicino itelligent light.""" diff --git a/src/pyatmo/modules/device_types.py b/src/pyatmo/modules/device_types.py index baa34f0b..71873d9a 100644 --- a/src/pyatmo/modules/device_types.py +++ b/src/pyatmo/modules/device_types.py @@ -1,8 +1,8 @@ """Definitions of Netatmo devices types.""" from __future__ import annotations -import logging from enum import Enum +import logging LOG = logging.getLogger(__name__) @@ -59,6 +59,7 @@ class DeviceType(str, Enum): NLP = "NLP" # Plug NLPBS = "NLPBS" # British standard plugs NLPC = "NLPC" # Connected energy meter + NLPD = "NLPD" # Dry contact NLPM = "NLPM" # mobile plug NLPO = "NLPO" # Connected contactor NLPS = "NLPS" # Smart Load Shedder @@ -72,14 +73,25 @@ class DeviceType(str, Enum): NLUF = "NLUF" # Legrand device stub NLAS = "NLAS" # Legrand wireless batteryless scene switch NLUP = "NLUP" # Legrand device stub - NLLF = "NLLF" # Legrand device stub + NLLF = "NLLF" # Legrand Centralized Ventilation Control NLTS = "NLTS" # Legrand motion sensor stub + NLJ = "NLJ" # Legrand garage door opener # BTicino Classe 300 EOS BNCX = "BNCX" # internal panel = gateway BNDL = "BNDL" # door lock BNEU = "BNEU" # external unit BNSL = "BNSL" # staircase light + BNCS = "BNCS" # Controlled Socket + BNXM = "BNXM" # X meter + BNMS = "BNMS" # motorized shade + BNAS = "BNAS" # automatic shutter + BNAB = "BNAB" # automatic blind + BNMH = "BNMH" # automatic blind + BNTH = "BNTH" # thermostat + BNFC = "BNFC" # fan coil + BNTR = "BNTR" # radiator + BNIL = "BNIL" # intelligent light # Bubbendorf shutters NBG = "NBG" # gateway @@ -95,8 +107,19 @@ class DeviceType(str, Enum): EBU = "EBU" # EBU gas meter Z3L = "Z3L" # Zigbee 3 Light + # Magellan + NLDP = "NLDP" # Pocket Remote + # pylint: enable=C0103 + @classmethod + def _missing_(cls, key): + """Handle unknown device types.""" + + msg = f"{key} device is unknown" + LOG.warning(msg) + return DeviceType.NLunknown + class DeviceCategory(str, Enum): """Class to represent Netatmo device types.""" @@ -115,6 +138,7 @@ class DeviceCategory(str, Enum): air_care = "air_care" meter = "meter" dimmer = "dimmer" + opening = "opening" # pylint: enable=C0103 @@ -124,6 +148,7 @@ class DeviceCategory(str, Enum): DeviceType.NATherm1: DeviceCategory.climate, DeviceType.OTM: DeviceCategory.climate, DeviceType.NOC: DeviceCategory.camera, + DeviceType.NACamDoorTag: DeviceCategory.opening, DeviceType.NACamera: DeviceCategory.camera, DeviceType.NDB: DeviceCategory.camera, DeviceType.NAMain: DeviceCategory.weather, @@ -156,6 +181,19 @@ class DeviceCategory(str, Enum): DeviceType.NLUO: DeviceCategory.dimmer, DeviceType.NLUI: DeviceCategory.switch, DeviceType.NLUF: DeviceCategory.dimmer, + DeviceType.NLPS: DeviceCategory.meter, + DeviceType.NLD: DeviceCategory.switch, + DeviceType.NLDD: DeviceCategory.switch, + DeviceType.NLPT: DeviceCategory.switch, + DeviceType.BNMS: DeviceCategory.shutter, + DeviceType.BNAS: DeviceCategory.shutter, + DeviceType.BNAB: DeviceCategory.shutter, + DeviceType.BNTH: DeviceCategory.climate, + DeviceType.BNFC: DeviceCategory.climate, + DeviceType.BNTR: DeviceCategory.climate, + DeviceType.NLPD: DeviceCategory.switch, + DeviceType.NLJ: DeviceCategory.shutter, + DeviceType.BNIL: DeviceCategory.switch, } @@ -212,11 +250,20 @@ class DeviceCategory(str, Enum): DeviceType.NLUI: ("Legrand", "In-wall switch"), DeviceType.NLTS: ("Legrand", "Motion sensor"), DeviceType.NLUF: ("Legrand", "In-Wall dimmer"), + DeviceType.NLJ: ("Legrand", "Garage door opener"), # BTicino Classe 300 EOS DeviceType.BNCX: ("BTicino", "Internal Panel"), DeviceType.BNEU: ("BTicino", "External Unit"), DeviceType.BNDL: ("BTicino", "Door Lock"), DeviceType.BNSL: ("BTicino", "Staircase Light"), + DeviceType.BNMS: ("BTicino", "Motorized Shade"), + DeviceType.BNAS: ("BTicino", "Automatic Shutter"), + DeviceType.BNAB: ("BTicino", "Automatic Blind"), + DeviceType.BNMH: ("BTicino", "MyHome server 1"), + DeviceType.BNTH: ("BTicino", "Thermostat"), + DeviceType.BNFC: ("BTicino", "Fan coil"), + DeviceType.BNTR: ("BTicino", "Module towel rail"), + DeviceType.BNIL: ("BTicino", "Intelligent light"), # Bubbendorf shutters DeviceType.NBG: ("Bubbendorf", "Gateway"), DeviceType.NBR: ("Bubbendorf", "Roller Shutter"), @@ -228,4 +275,5 @@ class DeviceCategory(str, Enum): DeviceType.BNS: ("Smarther", "Smarther with Netatmo"), DeviceType.Z3L: ("3rd Party", "Zigbee 3 Light"), DeviceType.EBU: ("3rd Party", "EBU gas meter"), + DeviceType.NLPD: ("Drivia", "Dry contact"), } diff --git a/src/pyatmo/modules/idiamant.py b/src/pyatmo/modules/idiamant.py index 9c273a4b..2753f0cf 100644 --- a/src/pyatmo/modules/idiamant.py +++ b/src/pyatmo/modules/idiamant.py @@ -15,16 +15,24 @@ class NBG(FirmwareMixin, WifiMixin, Module): + """Class to represent a iDiamant NBG.""" + ... class NBR(FirmwareMixin, RfMixin, ShutterMixin, Module): + """Class to represent a iDiamant NBR.""" + ... class NBO(FirmwareMixin, RfMixin, ShutterMixin, Module): + """Class to represent a iDiamant NBO.""" + ... class NBS(FirmwareMixin, RfMixin, ShutterMixin, Module): + """Class to represent a iDiamant NBS.""" + ... diff --git a/src/pyatmo/modules/legrand.py b/src/pyatmo/modules/legrand.py index 1b85137e..21d97dee 100644 --- a/src/pyatmo/modules/legrand.py +++ b/src/pyatmo/modules/legrand.py @@ -4,6 +4,7 @@ import logging from pyatmo.modules.module import ( + BatteryMixin, ContactorMixin, Dimmer, EnergyMixin, @@ -24,15 +25,15 @@ # pylint: disable=R0901 -class NLG(FirmwareMixin, Module): +class NLG(FirmwareMixin, OffloadMixin, WifiMixin, Module): """Legrand gateway.""" -class NLT(FirmwareMixin, Module): +class NLT(FirmwareMixin, BatteryMixin, Module): """Legrand global remote control.""" -class NLP(Switch): +class NLP(Switch, HistoryMixin, PowerMixin, OffloadMixin, Module): """Legrand plug.""" @@ -73,7 +74,7 @@ class NLIS(Switch): class NLD(Dimmer): - """Legrand Double On/Off dimmer remote""" + """Legrand Double On/Off dimmer remote.""" class NLL(FirmwareMixin, EnergyMixin, WifiMixin, SwitchMixin, Module): @@ -104,7 +105,7 @@ class NLPS(FirmwareMixin, PowerMixin, EnergyMixin, Module): """Legrand / BTicino smart load shedder.""" -class NLC(FirmwareMixin, SwitchMixin, Module): +class NLC(FirmwareMixin, SwitchMixin, HistoryMixin, PowerMixin, OffloadMixin, Module): """Legrand / BTicino cable outlet.""" @@ -154,3 +155,11 @@ class EBU(Module): class NLTS(Module): """NLTS motion sensor.""" + + +class NLPD(FirmwareMixin, SwitchMixin, Module): + """NLPD dry contact.""" + + +class NLJ(FirmwareMixin, RfMixin, ShutterMixin, Module): + """Legrand garage door opener.""" diff --git a/src/pyatmo/modules/module.py b/src/pyatmo/modules/module.py index acfa4caf..95a94713 100644 --- a/src/pyatmo/modules/module.py +++ b/src/pyatmo/modules/module.py @@ -1,10 +1,10 @@ """Module to represent a Netatmo module.""" from __future__ import annotations -import logging -from datetime import datetime +from datetime import datetime, timezone from enum import Enum -from typing import TYPE_CHECKING, Any, Dict +import logging +from typing import TYPE_CHECKING, Any from aiohttp import ClientConnectorError @@ -19,7 +19,7 @@ LOG = logging.getLogger(__name__) -ModuleT = Dict[str, Any] +ModuleT = dict[str, Any] # Hide from features list ATTRIBUTE_FILTER = { @@ -45,6 +45,7 @@ def process_battery_state(data: str) -> int: """Process battery data and return percent (int) for display.""" + mapping = { "max": 100, "full": 90, @@ -57,26 +58,40 @@ def process_battery_state(data: str) -> int: class FirmwareMixin(EntityBase): + """Mixin for firmware data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize firmware mixin.""" super().__init__(home, module) # type: ignore # mypy issue 4335 self.firmware_revision: int | None = None self.firmware_name: str | None = None class WifiMixin(EntityBase): + """Mixin for wifi data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize wifi mixin.""" super().__init__(home, module) # type: ignore # mypy issue 4335 self.wifi_strength: int | None = None class RfMixin(EntityBase): + """Mixin for rf data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize rf mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.rf_strength: int | None = None class RainMixin(EntityBase): + """Mixin for rain data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize rain mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.rain: float | None = None self.sum_rain_1: float | None = None @@ -84,7 +99,11 @@ def __init__(self, home: Home, module: ModuleT): class WindMixin(EntityBase): + """Mixin for wind data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize wind mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.wind_strength: int | None = None self.wind_angle: int | None = None @@ -94,16 +113,19 @@ def __init__(self, home: Home, module: ModuleT): @property def wind_direction(self) -> str | None: """Return wind direction.""" + return None if self.wind_angle is None else process_angle(self.wind_angle) @property def gust_direction(self) -> str | None: """Return gust direction.""" + return None if self.gust_angle is None else process_angle(self.gust_angle) def process_angle(angle: int) -> str: """Process angle and return string for display.""" + if angle >= 330: return "N" if angle >= 300: @@ -122,7 +144,11 @@ def process_angle(angle: int) -> str: class TemperatureMixin(EntityBase): + """Mixin for temperature data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize temperature mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.temperature: float | None = None self.temp_min: float | None = None @@ -135,31 +161,51 @@ def __init__(self, home: Home, module: ModuleT): class HumidityMixin(EntityBase): + """Mixin for humidity data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize humidity mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.humidity: int | None = None class CO2Mixin(EntityBase): + """Mixin for CO2 data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize CO2 mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.co2: int | None = None class HealthIndexMixin(EntityBase): + """Mixin for health index data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize health index mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.health_idx: int | None = None class NoiseMixin(EntityBase): + """Mixin for noise data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize noise mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.noise: int | None = None class PressureMixin(EntityBase): + """Mixin for pressure data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize pressure mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.pressure: float | None = None self.absolute_pressure: float | None = None @@ -167,13 +213,21 @@ def __init__(self, home: Home, module: ModuleT): class BoilerMixin(EntityBase): + """Mixin for boiler data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize boiler mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.boiler_status: bool | None = None class BatteryMixin(EntityBase): + """Mixin for battery data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize battery mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.battery_state: str | None = None self.battery_level: int | None = None @@ -181,6 +235,8 @@ def __init__(self, home: Home, module: ModuleT): @property def battery(self) -> int: + """Return battery percent.""" + if self.battery_percent is not None: return self.battery_percent if self.battery_state is None: @@ -189,18 +245,27 @@ def battery(self) -> int: class PlaceMixin(EntityBase): + """Mixin for place data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize place mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.place: Place | None = None class DimmableMixin(EntityBase): + """Mixin for dimmable data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize dimmable mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.brightness: int | None = None async def async_set_brightness(self, brightness: int) -> bool: """Set brightness.""" + json_brightness = { "modules": [ { @@ -214,48 +279,77 @@ async def async_set_brightness(self, brightness: int) -> bool: class ApplianceTypeMixin(EntityBase): + """Mixin for appliance type data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize appliance type mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.appliance_type: str | None = None class EnergyMixin(EntityBase): + """Mixin for energy data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize energy mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.sum_energy_elec: int | None = None class PowerMixin(EntityBase): + """Mixin for power data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize power mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.power: int | None = None class EventMixin(EntityBase): + """Mixin for event data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize event mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.events: list[Event] = [] class ContactorMixin(EntityBase): + """Mixin for contactor data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize contactor mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.contactor_mode: str | None = None class OffloadMixin(EntityBase): + """Mixin for offload data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize offload mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.offload: bool | None = None class SwitchMixin(EntityBase): + """Mixin for switch data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize switch mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.on: bool | None = None async def async_set_switch(self, target_position: int) -> bool: """Set switch to target position.""" + json_switch = { "modules": [ { @@ -269,21 +363,28 @@ async def async_set_switch(self, target_position: int) -> bool: async def async_on(self) -> bool: """Switch on.""" + return await self.async_set_switch(True) async def async_off(self) -> bool: """Switch off.""" + return await self.async_set_switch(False) class ShutterMixin(EntityBase): + """Mixin for shutter data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize shutter mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.current_position: int | None = None self.target_position: int | None = None async def async_set_target_position(self, target_position: int) -> bool: """Set shutter to target position.""" + json_roller_shutter = { "modules": [ { @@ -297,19 +398,26 @@ async def async_set_target_position(self, target_position: int) -> bool: async def async_open(self) -> bool: """Open shutter.""" + return await self.async_set_target_position(100) async def async_close(self) -> bool: """Close shutter.""" + return await self.async_set_target_position(0) async def async_stop(self) -> bool: """Stop shutter.""" + return await self.async_set_target_position(-1) class CameraMixin(EntityBase): + """Mixin for camera data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize camera mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.sd_status: int | None = None self.vpn_url: str | None = None @@ -320,6 +428,7 @@ def __init__(self, home: Home, module: ModuleT): async def async_get_live_snapshot(self) -> bytes | None: """Fetch live camera image.""" + if not self.local_url and not self.vpn_url: return None resp = await self.home.auth.async_get_image( @@ -332,6 +441,7 @@ async def async_get_live_snapshot(self) -> bytes | None: async def async_update_camera_urls(self) -> None: """Update and validate the camera urls.""" + if self.device_type == "NDB": self.is_local = None @@ -349,6 +459,7 @@ async def async_update_camera_urls(self) -> None: async def _async_check_url(self, url: str) -> str | None: """Validate camera url.""" + try: resp = await self.home.auth.async_post_api_request( base_url=f"{url}", @@ -365,12 +476,17 @@ async def _async_check_url(self, url: str) -> str | None: class FloodlightMixin(EntityBase): + """Mixin for floodlight data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize floodlight mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.floodlight: str | None = None async def async_set_floodlight_state(self, state: str) -> bool: """Set floodlight state.""" + json_floodlight_state = { "modules": [ { @@ -383,30 +499,42 @@ async def async_set_floodlight_state(self, state: str) -> bool: async def async_floodlight_on(self) -> bool: """Turn on floodlight.""" + return await self.async_set_floodlight_state("on") async def async_floodlight_off(self) -> bool: """Turn off floodlight.""" + return await self.async_set_floodlight_state("off") async def async_floodlight_auto(self) -> bool: """Set floodlight to auto mode.""" + return await self.async_set_floodlight_state("auto") class StatusMixin(EntityBase): + """Mixin for status data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize status mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.status: str | None = None class MonitoringMixin(EntityBase): + """Mixin for monitoring data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize monitoring mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.monitoring: bool | None = None async def async_set_monitoring_state(self, state: str) -> bool: """Set monitoring state.""" + json_monitoring_state = { "modules": [ { @@ -419,14 +547,18 @@ async def async_set_monitoring_state(self, state: str) -> bool: async def async_monitoring_on(self) -> bool: """Turn on monitoring.""" + return await self.async_set_monitoring_state("on") async def async_monitoring_off(self) -> bool: """Turn off monitoring.""" + return await self.async_set_monitoring_state("off") class MeasureInterval(Enum): + """Measure interval.""" + HALF_HOUR = "30min" HOUR = "1hour" THREE_HOURS = "3hours" @@ -436,6 +568,8 @@ class MeasureInterval(Enum): class MeasureType(Enum): + """Measure type.""" + BOILERON = "boileron" BOILEROFF = "boileroff" SUM_BOILER_ON = "sum_boiler_on" @@ -451,7 +585,11 @@ class MeasureType(Enum): class HistoryMixin(EntityBase): + """Mixin for history data.""" + def __init__(self, home: Home, module: ModuleT): + """Initialize history mixin.""" + super().__init__(home, module) # type: ignore # mypy issue 4335 self.historical_data: list[dict[str, Any]] | None = None self.start_time: int | None = None @@ -463,15 +601,17 @@ async def async_update_measures( interval: MeasureInterval = MeasureInterval.HOUR, days: int = 7, ) -> None: + """Update historical data.""" + end_time = int(datetime.now().timestamp()) if start_time is None: start_time = end_time - days * 24 * 60 * 60 - data_point = MeasureType.SUM_ENERGY_ELEC_BASIC.name + data_point = MeasureType.SUM_ENERGY_ELEC_BASIC.value params = { "device_id": self.bridge, "module_id": self.entity_id, - "scale": interval.name, + "scale": interval.value, "type": data_point, "date_begin": start_time, "date_end": end_time, @@ -495,8 +635,8 @@ async def async_update_measures( self.historical_data.append( { "duration": interval_min, - "startTime": f"{datetime.utcfromtimestamp(start_time + 1).isoformat()}Z", - "endTime": f"{datetime.utcfromtimestamp(end_time).isoformat()}Z", + "startTime": f"{datetime.fromtimestamp(start_time + 1, tz=timezone.utc).isoformat().split('+')[0]}Z", + "endTime": f"{datetime.fromtimestamp(end_time, tz=timezone.utc).isoformat().split('+')[0]}Z", "Wh": value[0], }, ) @@ -516,8 +656,12 @@ class Module(NetatmoBase): features: set[str] def __init__(self, home: Home, module: ModuleT) -> None: + """Initialize a Netatmo module instance.""" + super().__init__(module) + self.device_type = DeviceType(module["type"]) + self.home = home self.room_id = module.get("room_id") self.reachable = module.get("reachable") @@ -527,6 +671,8 @@ def __init__(self, home: Home, module: ModuleT) -> None: self.features = set() async def update(self, raw_data: RawData) -> None: + """Update module with the latest data.""" + self.update_topology(raw_data) self.update_features() @@ -539,6 +685,8 @@ async def update(self, raw_data: RawData) -> None: self.home.rooms[module.room_id].update(raw_data) def update_features(self) -> None: + """Update features.""" + self.features.update({var for var in vars(self) if var not in ATTRIBUTE_FILTER}) if "battery_state" in vars(self) or "battery_percent" in vars(self): self.features.add("battery") @@ -558,20 +706,30 @@ class Camera( WifiMixin, Module, ): + """Class to represent a Netatmo camera.""" + async def update(self, raw_data: RawData) -> None: + """Update camera with the latest data.""" + await Module.update(self, raw_data) await self.async_update_camera_urls() class Switch(FirmwareMixin, PowerMixin, SwitchMixin, Module): + """Class to represent a Netatmo switch.""" + ... class Dimmer(DimmableMixin, Switch): + """Class to represent a Netatmo dimmer.""" + ... class Shutter(FirmwareMixin, ShutterMixin, Module): + """Class to represent a Netatmo shutter.""" + ... diff --git a/src/pyatmo/modules/netatmo.py b/src/pyatmo/modules/netatmo.py index e0ba98e3..30edc205 100644 --- a/src/pyatmo/modules/netatmo.py +++ b/src/pyatmo/modules/netatmo.py @@ -1,8 +1,8 @@ """Module to represent Netatmo modules.""" from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import Any from pyatmo.const import ( @@ -46,34 +46,50 @@ class NRV(FirmwareMixin, RfMixin, BatteryMixin, Module): + """Class to represent a Netatmo NRV.""" + ... class NATherm1(FirmwareMixin, RfMixin, BatteryMixin, BoilerMixin, Module): + """Class to represent a Netatmo NATherm1.""" + ... class NAPlug(FirmwareMixin, RfMixin, WifiMixin, Module): + """Class to represent a Netatmo NAPlug.""" + ... class OTH(FirmwareMixin, WifiMixin, Module): + """Class to represent a Netatmo OTH.""" + ... class OTM(FirmwareMixin, RfMixin, BatteryMixin, BoilerMixin, Module): + """Class to represent a Netatmo OTM.""" + ... class NACamera(Camera): + """Class to represent a Netatmo NACamera.""" + ... class NOC(FloodlightMixin, Camera): + """Class to represent a Netatmo NOC.""" + ... class NDB(Camera): + """Class to represent a Netatmo NDB.""" + ... @@ -88,6 +104,8 @@ class NAMain( PlaceMixin, Module, ): + """Class to represent a Netatmo NAMain.""" + ... @@ -100,14 +118,20 @@ class NAModule1( PlaceMixin, Module, ): + """Class to represent a Netatmo NAModule1.""" + ... class NAModule2(WindMixin, RfMixin, FirmwareMixin, BatteryMixin, PlaceMixin, Module): + """Class to represent a Netatmo NAModule2.""" + ... class NAModule3(RainMixin, RfMixin, FirmwareMixin, BatteryMixin, PlaceMixin, Module): + """Class to represent a Netatmo NAModule3.""" + ... @@ -121,6 +145,8 @@ class NAModule4( PlaceMixin, Module, ): + """Class to represent a Netatmo NAModule4.""" + ... @@ -136,10 +162,14 @@ class NHC( PlaceMixin, Module, ): + """Class to represent a Netatmo NHC.""" + ... class NACamDoorTag(StatusMixin, FirmwareMixin, BatteryMixin, RfMixin, Module): + """Class to represent a Netatmo NACamDoorTag.""" + ... @@ -151,6 +181,8 @@ class NIS( RfMixin, Module, ): + """Class to represent a Netatmo NIS.""" + ... @@ -158,6 +190,8 @@ class NSD( FirmwareMixin, Module, ): + """Class to represent a Netatmo NSD.""" + ... @@ -165,6 +199,8 @@ class NCO( FirmwareMixin, Module, ): + """Class to represent a Netatmo NCO.""" + ... @@ -179,6 +215,8 @@ class Location: class PublicWeatherArea: + """Class of Netatmo public weather data.""" + location: Location required_data_type: str | None filtering: bool @@ -193,6 +231,8 @@ def __init__( required_data_type: str | None = None, filtering: bool = False, ) -> None: + """Initialize self.""" + self.location = Location( lat_ne, lon_ne, @@ -205,43 +245,58 @@ def __init__( def update(self, raw_data: RawData) -> None: """Update public weather area with the latest data.""" + self.modules = list(raw_data.get("public", [])) def stations_in_area(self) -> int: """Return available number of stations in area.""" + return len(self.modules) def get_latest_rain(self) -> dict[str, Any]: + """Return latest rain measures.""" return self.get_accessory_data(ACCESSORY_RAIN_LIVE_TYPE) def get_60_min_rain(self) -> dict[str, Any]: + """Return 60 min rain measures.""" return self.get_accessory_data(ACCESSORY_RAIN_60MIN_TYPE) def get_24_h_rain(self) -> dict[str, Any]: + """Return 24 h rain measures.""" return self.get_accessory_data(ACCESSORY_RAIN_24H_TYPE) def get_latest_pressures(self) -> dict[str, Any]: + """Return latest pressure measures.""" return self.get_latest_station_measures(STATION_PRESSURE_TYPE) def get_latest_temperatures(self) -> dict[str, Any]: + """Return latest temperature measures.""" return self.get_latest_station_measures(STATION_TEMPERATURE_TYPE) def get_latest_humidities(self) -> dict[str, Any]: + """Return latest humidity measures.""" return self.get_latest_station_measures(STATION_HUMIDITY_TYPE) def get_latest_wind_strengths(self) -> dict[str, Any]: + """Return latest wind strength measures.""" return self.get_accessory_data(ACCESSORY_WIND_STRENGTH_TYPE) def get_latest_wind_angles(self) -> dict[str, Any]: + """Return latest wind angle measures.""" return self.get_accessory_data(ACCESSORY_WIND_ANGLE_TYPE) def get_latest_gust_strengths(self) -> dict[str, Any]: + """Return latest gust strength measures.""" return self.get_accessory_data(ACCESSORY_GUST_STRENGTH_TYPE) def get_latest_gust_angles(self) -> dict[str, Any]: + """Return latest gust angle measures.""" + return self.get_accessory_data(ACCESSORY_GUST_ANGLE_TYPE) def get_latest_station_measures(self, data_type: str) -> dict[str, Any]: + """Return latest station measures of a given type.""" + measures: dict[str, Any] = {} for station in self.modules: for module in station["measures"].values(): @@ -260,6 +315,8 @@ def get_latest_station_measures(self, data_type: str) -> dict[str, Any]: return measures def get_accessory_data(self, data_type: str) -> dict[str, Any]: + """Return accessory data of a given type.""" + data: dict[str, Any] = {} for station in self.modules: for module in station["measures"].values(): diff --git a/src/pyatmo/modules/somfy.py b/src/pyatmo/modules/somfy.py index 88d026c5..d364f795 100644 --- a/src/pyatmo/modules/somfy.py +++ b/src/pyatmo/modules/somfy.py @@ -9,4 +9,6 @@ class TPSRS(FirmwareMixin, RfMixin, ShutterMixin, Module): + """Class to represent a somfy TPSRS.""" + ... diff --git a/src/pyatmo/person.py b/src/pyatmo/person.py index 9fd2ac8e..bb4cbf12 100644 --- a/src/pyatmo/person.py +++ b/src/pyatmo/person.py @@ -1,8 +1,8 @@ """Module to represent a Netatmo person.""" from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import TYPE_CHECKING from pyatmo.const import RawData @@ -22,6 +22,8 @@ class Person(NetatmoBase): url: str | None def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize a Netatmo person instance.""" + super().__init__(raw_data) self.home = home self.pseudo = raw_data.get("pseudo") diff --git a/src/pyatmo/public_data.py b/src/pyatmo/public_data.py index 671811e5..117f8628 100644 --- a/src/pyatmo/public_data.py +++ b/src/pyatmo/public_data.py @@ -1,9 +1,9 @@ """Support for Netatmo public weather data.""" from __future__ import annotations -import dataclasses from abc import ABC from collections import defaultdict +import dataclasses from typing import Any from warnings import warn @@ -37,77 +37,124 @@ class AbstractPublicData(ABC): def process(self, resp: dict) -> None: """Process data from API.""" + self.status = resp.get("status", "") def stations_in_area(self) -> int: + """Return number of stations in area.""" + return len(self.raw_data) def get_latest_rain(self) -> dict: + """Return latest rain measures.""" + return self.get_accessory_data(ACCESSORY_RAIN_LIVE_TYPE) def get_average_rain(self) -> float: + """Return average rain measures.""" + return average(self.get_latest_rain()) def get_60_min_rain(self) -> dict: + """Return 60 min rain measures.""" + return self.get_accessory_data(ACCESSORY_RAIN_60MIN_TYPE) def get_average_60_min_rain(self) -> float: + """Return average 60 min rain measures.""" + return average(self.get_60_min_rain()) def get_24_h_rain(self) -> dict: + """Return 24 h rain measures.""" + return self.get_accessory_data(ACCESSORY_RAIN_24H_TYPE) def get_average_24_h_rain(self) -> float: + """Return average 24 h rain measures.""" + return average(self.get_24_h_rain()) def get_latest_pressures(self) -> dict: + """Return latest pressure measures.""" + return self.get_latest_station_measures(STATION_PRESSURE_TYPE) def get_average_pressure(self) -> float: + """Return average pressure measures.""" + return average(self.get_latest_pressures()) def get_latest_temperatures(self) -> dict: + """Return latest temperature measures.""" + return self.get_latest_station_measures(STATION_TEMPERATURE_TYPE) def get_average_temperature(self) -> float: + """Return average temperature measures.""" + return average(self.get_latest_temperatures()) def get_latest_humidities(self) -> dict: + """Return latest humidity measures.""" + return self.get_latest_station_measures(STATION_HUMIDITY_TYPE) def get_average_humidity(self) -> float: + """Return average humidity measures.""" + return average(self.get_latest_humidities()) def get_latest_wind_strengths(self) -> dict: + """Return latest wind strengths.""" + return self.get_accessory_data(ACCESSORY_WIND_STRENGTH_TYPE) def get_average_wind_strength(self) -> float: + """Return average wind strength.""" + return average(self.get_latest_wind_strengths()) def get_latest_wind_angles(self) -> dict: + """Return latest wind angles.""" + return self.get_accessory_data(ACCESSORY_WIND_ANGLE_TYPE) def get_latest_gust_strengths(self) -> dict: + """Return latest gust strengths.""" + return self.get_accessory_data(ACCESSORY_GUST_STRENGTH_TYPE) def get_average_gust_strength(self) -> float: + """Return average gust strength.""" + return average(self.get_latest_gust_strengths()) def get_latest_gust_angles(self): + """Return latest gust angles.""" + return self.get_accessory_data(ACCESSORY_GUST_ANGLE_TYPE) def get_locations(self) -> dict: + """Return locations of stations.""" + return { station["_id"]: station["place"]["location"] for station in self.raw_data } def get_time_for_rain_measures(self) -> dict: + """Return time for rain measures.""" + return self.get_accessory_data(ACCESSORY_RAIN_TIME_TYPE) def get_time_for_wind_measures(self) -> dict: + """Return time for wind measures.""" + return self.get_accessory_data(ACCESSORY_WIND_TIME_TYPE) def get_latest_station_measures(self, data_type) -> dict: + """Return latest station measures of a given type.""" + measures: dict = {} for station in self.raw_data: for module in station["measures"].values(): @@ -126,6 +173,8 @@ def get_latest_station_measures(self, data_type) -> dict: return measures def get_accessory_data(self, data_type: str) -> dict[str, Any]: + """Return all accessory data of a given type.""" + data: dict = {} for station in self.raw_data: for module in station["measures"].values(): @@ -148,18 +197,8 @@ def __init__( required_data_type: str | None = None, filtering: bool = False, ) -> None: - """Initialize self. - - Arguments: - auth {NetatmoOAuth2} -- Authentication information with a valid access token - LAT_NE {str} -- Latitude of the north-east corner of the requested area. (-85 <= LAT_NE <= 85 and LAT_NE > LAT_SW) - LON_NE {str} -- Longitude of the north-east corner of the requested area. (-180 <= LON_NE <= 180 and LON_NE > LON_SW) - LAT_SW {str} -- latitude of the south-west corner of the requested area. (-85 <= LAT_SW <= 85) - LON_SW {str} -- Longitude of the south-west corner of the requested area. (-180 <= LON_SW <= 180) - - Keyword Arguments: - required_data_type {str} -- comma-separated list from above _STATION or _ACCESSORY values (default: {None}) - """ + """Initialize self.""" + self.auth = auth self.required_data_type = required_data_type self.location = Location(lat_ne, lon_ne, lat_sw, lon_sw) @@ -167,6 +206,7 @@ def __init__( def update(self) -> None: """Fetch and process data from API.""" + post_params: dict = { **dataclasses.asdict(self.location), "filter": self.filtering, @@ -200,18 +240,8 @@ def __init__( required_data_type: str | None = None, filtering: bool = False, ) -> None: - """Initialize self. - - Arguments: - auth {AbstractAsyncAuth} -- Authentication information with a valid access token - LAT_NE {str} -- Latitude of the north-east corner of the requested area. (-85 <= LAT_NE <= 85 and LAT_NE > LAT_SW) - LON_NE {str} -- Longitude of the north-east corner of the requested area. (-180 <= LON_NE <= 180 and LON_NE > LON_SW) - LAT_SW {str} -- latitude of the south-west corner of the requested area. (-85 <= LAT_SW <= 85) - LON_SW {str} -- Longitude of the south-west corner of the requested area. (-180 <= LON_SW <= 180) - - Keyword Arguments: - required_data_type {str} -- comma-separated list from above _STATION or _ACCESSORY values (default: {None}) - """ + """Initialize self.""" + self.auth = auth self.required_data_type = required_data_type self.location = Location(lat_ne, lon_ne, lat_sw, lon_sw) @@ -219,6 +249,7 @@ def __init__( async def async_update(self) -> None: """Fetch and process data from API.""" + post_params: dict = { **dataclasses.asdict(self.location), "filter": self.filtering, @@ -242,4 +273,6 @@ async def async_update(self) -> None: def average(data: dict) -> float: + """Calculate average value of a dict.""" + return sum(data.values()) / len(data) if data else 0.0 diff --git a/src/pyatmo/room.py b/src/pyatmo/room.py index 66acf808..6c2979a2 100644 --- a/src/pyatmo/room.py +++ b/src/pyatmo/room.py @@ -1,8 +1,8 @@ """Module to represent a Netatmo room.""" from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import TYPE_CHECKING, Any from pyatmo.const import FROSTGUARD, HOME, MANUAL, SETROOMTHERMPOINT_ENDPOINT, RawData @@ -41,6 +41,8 @@ def __init__( room: dict[str, Any], all_modules: dict[str, Module], ) -> None: + """Initialize a Netatmo room instance.""" + super().__init__(room) self.home = home self.modules = { @@ -53,6 +55,8 @@ def __init__( self.evaluate_device_type() def update_topology(self, raw_data: RawData) -> None: + """Update room topology.""" + self.name = raw_data["name"] self.modules = { m_id: m @@ -62,6 +66,8 @@ def update_topology(self, raw_data: RawData) -> None: self.evaluate_device_type() def evaluate_device_type(self) -> None: + """Evaluate the device type of the room.""" + for module in self.modules.values(): self.device_types.add(module.device_type) if module.device_category is not None: @@ -78,6 +84,8 @@ def evaluate_device_type(self) -> None: self.features.add("humidity") def update(self, raw_data: RawData) -> None: + """Update room data.""" + self.heating_power_request = raw_data.get("heating_power_request") self.humidity = raw_data.get("humidity") self.reachable = raw_data.get("reachable") @@ -90,12 +98,18 @@ async def async_therm_manual( temp: float | None = None, end_time: int | None = None, ) -> None: + """Set room temperature set point to manual.""" + await self.async_therm_set(MANUAL, temp, end_time) async def async_therm_home(self, end_time: int | None = None) -> None: + """Set room temperature set point to home.""" + await self.async_therm_set(HOME, end_time=end_time) async def async_therm_frostguard(self, end_time: int | None = None) -> None: + """Set room temperature set point to frostguard.""" + await self.async_therm_set(FROSTGUARD, end_time=end_time) async def async_therm_set( @@ -105,6 +119,7 @@ async def async_therm_set( end_time: int | None = None, ) -> None: """Set room temperature set point.""" + mode = MODE_MAP.get(mode, mode) if "NATherm1" in self.device_types or ( @@ -121,6 +136,8 @@ async def _async_therm_set( temp: float | None = None, end_time: int | None = None, ) -> bool: + """Set room temperature set point (OTM).""" + json_therm_set: dict[str, Any] = { "rooms": [ { @@ -145,6 +162,7 @@ async def _async_set_thermpoint( end_time: int | None = None, ) -> None: """Set room temperature set point (NRV, NATherm1).""" + post_params = { "home_id": self.home.entity_id, "room_id": self.entity_id, diff --git a/src/pyatmo/schedule.py b/src/pyatmo/schedule.py index 903d5583..3b90336b 100644 --- a/src/pyatmo/schedule.py +++ b/src/pyatmo/schedule.py @@ -1,12 +1,13 @@ """Module to represent a Netatmo schedule.""" from __future__ import annotations -import logging from dataclasses import dataclass +import logging from typing import TYPE_CHECKING from pyatmo.const import RawData from pyatmo.modules.base_class import NetatmoBase +from pyatmo.room import Room if TYPE_CHECKING: from .home import Home @@ -21,10 +22,51 @@ class Schedule(NetatmoBase): selected: bool away_temp: float | None hg_temp: float | None + timetable: list[TimetableEntry] def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize a Netatmo schedule instance.""" super().__init__(raw_data) self.home = home self.selected = raw_data.get("selected", False) self.hg_temp = raw_data.get("hg_temp") self.away_temp = raw_data.get("away_temp") + self.timetable = [ + TimetableEntry(home, r) for r in raw_data.get("timetable", []) + ] + self.zones = [Zone(home, r) for r in raw_data.get("zones", [])] + + +@dataclass +class TimetableEntry: + """Class to represent a Netatmo schedule's timetable entry.""" + + zone_id: int | None + m_offset: int | None + + def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize a Netatmo schedule's timetable entry instance.""" + self.home = home + self.zone_id = raw_data.get("zone_id", 0) + self.m_offset = raw_data.get("m_offset", 0) + + +@dataclass +class Zone(NetatmoBase): + """Class to represent a Netatmo schedule's zone.""" + + type: int + rooms: list[Room] + + def __init__(self, home: Home, raw_data: RawData) -> None: + """Initialize a Netatmo schedule's zone instance.""" + super().__init__(raw_data) + self.home = home + self.type = raw_data.get("type", 0) + + def room_factory(home: Home, room_raw_data: RawData): + room = Room(home, room_raw_data, {}) + room.update(room_raw_data) + return room + + self.rooms = [room_factory(home, r) for r in raw_data.get("rooms", [])] diff --git a/src/pyatmo/thermostat.py b/src/pyatmo/thermostat.py index c9c66fed..bfd59ba6 100644 --- a/src/pyatmo/thermostat.py +++ b/src/pyatmo/thermostat.py @@ -1,9 +1,9 @@ """Support for Netatmo energy devices (relays, thermostats and valves).""" from __future__ import annotations -import logging from abc import ABC from collections import defaultdict +import logging from typing import Any from warnings import warn @@ -36,6 +36,7 @@ class AbstractHomeData(ABC): def process(self) -> None: """Process data from API.""" + self.homes = {d["id"]: d for d in self.raw_data} for item in self.raw_data: @@ -70,25 +71,29 @@ def process(self) -> None: def _get_selected_schedule(self, home_id: str) -> dict: """Get the selected schedule for a given home ID.""" + return next( ( value for value in self.schedules.get(home_id, {}).values() - if "selected" in value.keys() + if "selected" in value ), {}, ) def get_hg_temp(self, home_id: str) -> float | None: """Return frost guard temperature value.""" + return self._get_selected_schedule(home_id).get("hg_temp") def get_away_temp(self, home_id: str) -> float | None: """Return the configured away temperature value.""" + return self._get_selected_schedule(home_id).get("away_temp") def get_thermostat_type(self, home_id: str, room_id: str) -> str | None: """Return the thermostat type of the room.""" + return next( ( module.get("type") @@ -100,6 +105,7 @@ def get_thermostat_type(self, home_id: str, room_id: str) -> str | None: def is_valid_schedule(self, home_id: str, schedule_id: str): """Check if valid schedule.""" + schedules = ( self.schedules[home_id][s]["id"] for s in self.schedules.get(home_id, {}) ) @@ -110,15 +116,13 @@ class HomeData(AbstractHomeData): """Class of Netatmo energy devices.""" def __init__(self, auth: NetatmoOAuth2) -> None: - """Initialize the Netatmo home data. + """Initialize the Netatmo home data.""" - Arguments: - auth {NetatmoOAuth2} -- Authentication information with valid access token - """ self.auth = auth def update(self) -> None: """Fetch and process data from API.""" + resp = self.auth.post_api_request(endpoint=GETHOMESDATA_ENDPOINT) self.raw_data = extract_raw_data(resp.json(), "homes") @@ -126,6 +130,7 @@ def update(self) -> None: def switch_home_schedule(self, home_id: str, schedule_id: str) -> Any: """Switch the schedule for a give home ID.""" + if not self.is_valid_schedule(home_id, schedule_id): raise NoSchedule(f"{schedule_id} is not a valid schedule id") @@ -141,15 +146,13 @@ class AsyncHomeData(AbstractHomeData): """Class of Netatmo energy devices.""" def __init__(self, auth: AbstractAsyncAuth) -> None: - """Initialize the Netatmo home data. + """Initialize the Netatmo home data.""" - Arguments: - auth {AbstractAsyncAuth} -- Authentication information with valid access token - """ self.auth = auth async def async_update(self): """Fetch and process data from API.""" + resp = await self.auth.async_post_api_request(endpoint=GETHOMESDATA_ENDPOINT) assert not isinstance(resp, bytes) @@ -158,6 +161,7 @@ async def async_update(self): async def async_switch_home_schedule(self, home_id: str, schedule_id: str) -> None: """Switch the schedule for a give home ID.""" + if not self.is_valid_schedule(home_id, schedule_id): raise NoSchedule(f"{schedule_id} is not a valid schedule id") @@ -179,6 +183,7 @@ class AbstractHomeStatus(ABC): def process(self) -> None: """Process data from API.""" + for room in self.raw_data.get("rooms", []): self.rooms[room["id"]] = room @@ -194,6 +199,7 @@ def process(self) -> None: def get_room(self, room_id: str) -> dict: """Return room data for a given room id.""" + for value in self.rooms.values(): if value["id"] == room_id: return value @@ -202,6 +208,7 @@ def get_room(self, room_id: str) -> dict: def get_thermostat(self, room_id: str) -> dict: """Return thermostat data for a given room id.""" + for value in self.thermostats.values(): if value["id"] == room_id: return value @@ -210,6 +217,7 @@ def get_thermostat(self, room_id: str) -> dict: def get_relay(self, room_id: str) -> dict: """Return relay data for a given room id.""" + for value in self.relays.values(): if value["id"] == room_id: return value @@ -218,6 +226,7 @@ def get_relay(self, room_id: str) -> dict: def get_valve(self, room_id: str) -> dict: """Return valve data for a given room id.""" + for value in self.valves.values(): if value["id"] == room_id: return value @@ -226,18 +235,22 @@ def get_valve(self, room_id: str) -> dict: def set_point(self, room_id: str) -> float | None: """Return the setpoint of a given room.""" + return self.get_room(room_id).get("therm_setpoint_temperature") def set_point_mode(self, room_id: str) -> str | None: """Return the setpointmode of a given room.""" + return self.get_room(room_id).get("therm_setpoint_mode") def measured_temperature(self, room_id: str) -> float | None: """Return the measured temperature of a given room.""" + return self.get_room(room_id).get("therm_measured_temperature") def boiler_status(self, module_id: str) -> bool | None: """Return the status of the boiler status.""" + return self.get_thermostat(module_id).get("boiler_status") @@ -245,17 +258,14 @@ class HomeStatus(AbstractHomeStatus): """Class of the Netatmo home status.""" def __init__(self, auth: NetatmoOAuth2, home_id: str): - """Initialize the Netatmo home status. + """Initialize the Netatmo home status.""" - Arguments: - auth {NetatmoOAuth2} -- Authentication information with a valid access token - home_id {str} -- ID for targeted home - """ self.auth = auth self.home_id = home_id def update(self) -> None: """Fetch and process data from API.""" + resp = self.auth.post_api_request( endpoint=GETHOMESTATUS_ENDPOINT, params={"home_id": self.home_id}, @@ -271,6 +281,7 @@ def set_thermmode( schedule_id: str | None = None, ) -> str | None: """Set thermotat mode.""" + post_params = {"home_id": self.home_id, "mode": mode} if end_time is not None and mode in {"hg", "away"}: post_params["endtime"] = str(end_time) @@ -291,6 +302,7 @@ def set_room_thermpoint( end_time: int | None = None, ) -> str | None: """Set room themperature set point.""" + post_params = {"home_id": self.home_id, "room_id": room_id, "mode": mode} # Temp and endtime should only be sent when mode=='manual', but netatmo api can # handle that even when mode == 'home' and these settings don't make sense @@ -310,17 +322,14 @@ class AsyncHomeStatus(AbstractHomeStatus): """Class of the Netatmo home status.""" def __init__(self, auth: AbstractAsyncAuth, home_id: str): - """Initialize the Netatmo home status. + """Initialize the Netatmo home status.""" - Arguments: - auth {AbstractAsyncAuth} -- Authentication information with a valid access token - home_id {str} -- ID for targeted home - """ self.auth = auth self.home_id = home_id async def async_update(self) -> None: """Fetch and process data from API.""" + resp = await self.auth.async_post_api_request( endpoint=GETHOMESTATUS_ENDPOINT, params={"home_id": self.home_id}, @@ -337,6 +346,7 @@ async def async_set_thermmode( schedule_id: str | None = None, ) -> str | None: """Set thermotat mode.""" + post_params = {"home_id": self.home_id, "mode": mode} if end_time is not None and mode in {"hg", "away"}: post_params["endtime"] = str(end_time) @@ -359,6 +369,7 @@ async def async_set_room_thermpoint( end_time: int | None = None, ) -> str | None: """Set room themperature set point.""" + post_params = {"home_id": self.home_id, "room_id": room_id, "mode": mode} # Temp and endtime should only be sent when mode=='manual', but netatmo api can # handle that even when mode == 'home' and these settings don't make sense diff --git a/src/pyatmo/weather_station.py b/src/pyatmo/weather_station.py index db0c16d8..14b9ca71 100644 --- a/src/pyatmo/weather_station.py +++ b/src/pyatmo/weather_station.py @@ -1,10 +1,10 @@ """Support for Netatmo weather station devices (stations and modules).""" from __future__ import annotations -import logging -import time from abc import ABC from collections import defaultdict +import logging +import time from warnings import warn from pyatmo.auth import AbstractAsyncAuth, NetatmoOAuth2 @@ -26,6 +26,7 @@ class AbstractWeatherStationData(ABC): def process(self) -> None: """Process data from API.""" + self.stations = {d["_id"]: d for d in self.raw_data} self.modules = {} @@ -46,6 +47,7 @@ def process(self) -> None: def get_module_names(self, station_id: str) -> list: """Return a list of all module names for a given station.""" + if not (station_data := self.get_station(station_id)): return [] @@ -58,6 +60,7 @@ def get_module_names(self, station_id: str) -> list: def get_modules(self, station_id: str) -> dict: """Return a dict of modules per given station.""" + if not (station_data := self.get_station(station_id)): return {} @@ -82,14 +85,17 @@ def get_modules(self, station_id: str) -> dict: def get_station(self, station_id: str) -> dict: """Return station by id.""" + return self.stations.get(station_id, {}) def get_module(self, module_id: str) -> dict: """Return module by id.""" + return self.modules.get(module_id, {}) def get_monitored_conditions(self, module_id: str) -> list: """Return monitored conditions for given module.""" + if not (module := (self.get_module(module_id) or self.get_station(module_id))): return [] @@ -135,8 +141,8 @@ def get_monitored_conditions(self, module_id: str) -> list: def get_last_data(self, station_id: str, exclude: int = 0) -> dict: """Return data for a given station and time frame.""" - key = "_id" + key = "_id" last_data: dict = {} if ( @@ -157,7 +163,6 @@ def get_last_data(self, station_id: str, exclude: int = 0) -> dict: last_data[station[key]]["reachable"] = station.get("reachable") for module in station["modules"]: - if "dashboard_data" not in module or key not in module: continue @@ -182,6 +187,7 @@ def get_last_data(self, station_id: str, exclude: int = 0) -> dict: def check_not_updated(self, station_id: str, delay: int = 3600) -> list: """Check if a given station has not been updated.""" + res = self.get_last_data(station_id) return [ key for key, value in res.items() if time.time() - value["When"] > delay @@ -189,6 +195,7 @@ def check_not_updated(self, station_id: str, delay: int = 3600) -> list: def check_updated(self, station_id: str, delay: int = 3600) -> list: """Check if a given station has been updated.""" + res = self.get_last_data(station_id) return [ key for key, value in res.items() if time.time() - value["When"] < delay @@ -204,18 +211,15 @@ def __init__( endpoint: str = GETSTATIONDATA_ENDPOINT, favorites: bool = True, ) -> None: - """Initialize the Netatmo weather station data. + """Initialize the Netatmo weather station data.""" - Arguments: - auth {NetatmoOAuth2} -- Authentication information with a valid access token - url_req {str} -- Optional request endpoint - """ self.auth = auth self.endpoint = endpoint self.params = {"get_favorites": ("true" if favorites else "false")} def update(self): """Fetch data from API.""" + self.raw_data = extract_raw_data( self.auth.post_api_request( endpoint=self.endpoint, @@ -238,6 +242,7 @@ def get_data( real_time: bool = False, ) -> dict | None: """Retrieve data from a device or module.""" + post_params = {"device_id": device_id} if module_id: post_params["module_id"] = module_id @@ -268,18 +273,8 @@ def get_min_max_t_h( module_id: str | None = None, frame: str = "last24", ) -> tuple[float, float, float, float] | None: - """Return minimum and maximum temperature and humidity over the given timeframe. - - Arguments: - station_id {str} -- Station ID + """Return minimum and maximum temperature and humidity over the given timeframe.""" - Keyword Arguments: - module_id {str} -- Module ID (default: {None}) - frame {str} -- Timeframe can be "last24" or "day" (default: {"last24"}) - - Returns: - (min_t {float}, max_t {float}, min_h {float}, max_h {float}) -- minimum and maximum for temperature and humidity - """ if frame == "last24": end = time.time() start = end - 24 * 3600 # 24 hours ago @@ -314,18 +309,15 @@ def __init__( endpoint: str = GETSTATIONDATA_ENDPOINT, favorites: bool = True, ) -> None: - """Initialize the Netatmo weather station data. + """Initialize the Netatmo weather station data.""" - Arguments: - auth {AbstractAsyncAuth} -- Authentication information with a valid access token - url_req {str} -- Optional request endpoint - """ self.auth = auth self.endpoint = endpoint self.params = {"get_favorites": ("true" if favorites else "false")} async def async_update(self): """Fetch data from API.""" + resp = await self.auth.async_post_api_request( endpoint=self.endpoint, params=self.params, diff --git a/tests/conftest.py b/tests/conftest.py index ff5a6056..a312ff0b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,11 @@ """Define shared fixtures.""" # pylint: disable=redefined-outer-name, protected-access -import json from contextlib import contextmanager +import json from unittest.mock import AsyncMock, patch -import pytest - import pyatmo +import pytest from .common import MockResponse, fake_post_request diff --git a/tests/test_async.py b/tests/test_async.py index 18de65e6..cef46766 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -3,9 +3,9 @@ import json from unittest.mock import AsyncMock, patch +import pyatmo import pytest -import pyatmo from tests.conftest import MockResponse, does_not_raise LON_NE = "6.221652" @@ -123,7 +123,6 @@ async def test_async_public_data_error(async_auth): "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", AsyncMock(return_value=mock_resp), ): - public_data = pyatmo.AsyncPublicData(async_auth, LAT_NE, LON_NE, LAT_SW, LON_SW) with pytest.raises(pyatmo.NoDevice): @@ -186,10 +185,9 @@ async def test_async_home_data_no_data(async_auth): with patch( "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", AsyncMock(return_value=mock_resp), - ): - with pytest.raises(pyatmo.NoDevice): - home_data = pyatmo.AsyncHomeData(async_auth) - await home_data.async_update() + ), pytest.raises(pyatmo.NoDevice): + home_data = pyatmo.AsyncHomeData(async_auth) + await home_data.async_update() @pytest.mark.asyncio @@ -232,12 +230,11 @@ async def test_async_home_data_switch_home_schedule( with patch( "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", AsyncMock(return_value=json_fixture), - ): - with expected: - await async_home_data.async_switch_home_schedule( - home_id=t_home_id, - schedule_id=t_sched_id, - ) + ), expected: + await async_home_data.async_switch_home_schedule( + home_id=t_home_id, + schedule_id=t_sched_id, + ) @pytest.mark.parametrize( diff --git a/tests/test_pyatmo.py b/tests/test_pyatmo.py index 7e34141b..c4d76d76 100644 --- a/tests/test_pyatmo.py +++ b/tests/test_pyatmo.py @@ -4,9 +4,8 @@ import time import oauthlib -import pytest - import pyatmo +import pytest def test_client_auth(auth): diff --git a/tests/test_pyatmo_camera.py b/tests/test_pyatmo_camera.py index d6e60b16..d10d047e 100644 --- a/tests/test_pyatmo_camera.py +++ b/tests/test_pyatmo_camera.py @@ -3,11 +3,10 @@ import datetime as dt import json +import pyatmo import pytest import time_machine -import pyatmo - from .conftest import does_not_raise diff --git a/tests/test_pyatmo_homecoach.py b/tests/test_pyatmo_homecoach.py index 22e88cb3..2fa475e1 100644 --- a/tests/test_pyatmo_homecoach.py +++ b/tests/test_pyatmo_homecoach.py @@ -2,9 +2,8 @@ # pylint: disable=protected-access import json -import pytest - import pyatmo +import pytest def test_home_coach_data(home_coach_data): diff --git a/tests/test_pyatmo_publicdata.py b/tests/test_pyatmo_publicdata.py index 53c837a0..c844e8ec 100644 --- a/tests/test_pyatmo_publicdata.py +++ b/tests/test_pyatmo_publicdata.py @@ -2,9 +2,8 @@ # pylint: disable=protected-access import json -import pytest - import pyatmo +import pytest LON_NE = "6.221652" LAT_NE = "46.610870" diff --git a/tests/test_pyatmo_refactor.py b/tests/test_pyatmo_refactor.py index d048a508..956f5cb7 100644 --- a/tests/test_pyatmo_refactor.py +++ b/tests/test_pyatmo_refactor.py @@ -3,14 +3,14 @@ import json from unittest.mock import AsyncMock, patch -import pytest -import time_machine - import pyatmo from pyatmo import DeviceType, NoDevice, NoSchedule from pyatmo.modules import NATherm1 from pyatmo.modules.base_class import Location, Place from pyatmo.modules.device_types import DeviceCategory +import pytest +import time_machine + from tests.common import fake_post_request from tests.conftest import MockResponse, does_not_raise @@ -326,11 +326,10 @@ async def test_async_climate_switch_schedule( with patch( "pyatmo.auth.AbstractAsyncAuth.async_post_api_request", AsyncMock(return_value=MockResponse(response, 200)), - ): - with expected: - await async_home.async_switch_schedule( - schedule_id=t_sched_id, - ) + ), expected: + await async_home.async_switch_schedule( + schedule_id=t_sched_id, + ) @pytest.mark.asyncio @@ -1129,3 +1128,10 @@ async def test_historical_data_retrieval(async_account): "endTime": "2022-02-12T08:29:49Z", } assert len(module.historical_data) == 168 + + +def test_device_types_missing(): + """Test handling of missing device types.""" + + assert DeviceType("NOC") == DeviceType.NOC + assert DeviceType("UNKNOWN") == DeviceType.NLunknown diff --git a/tests/test_pyatmo_thermostat.py b/tests/test_pyatmo_thermostat.py index 6c84c69d..4d9e2651 100644 --- a/tests/test_pyatmo_thermostat.py +++ b/tests/test_pyatmo_thermostat.py @@ -2,9 +2,9 @@ # pylint: disable=protected-access import json +import pyatmo import pytest -import pyatmo from tests.conftest import does_not_raise diff --git a/tests/test_pyatmo_weatherstation.py b/tests/test_pyatmo_weatherstation.py index 78f4899a..5019a126 100644 --- a/tests/test_pyatmo_weatherstation.py +++ b/tests/test_pyatmo_weatherstation.py @@ -3,11 +3,10 @@ import datetime as dt import json +import pyatmo import pytest import time_machine -import pyatmo - def test_weather_station_data(weather_station_data): assert ( diff --git a/tox.ini b/tox.ini index 3b23687b..078d13e0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,12 @@ [tox] -envlist = py38,py39,py310 +envlist = py310,py311 isolated_build = True skip_missing_interpreters = True [gh-actions] python = - 3.8: py38 - 3.9: py39 3.10: py310 + 3.11: py311 [testenv] deps =