pax_global_header00006660000000000000000000000064145736216260014526gustar00rootroot0000000000000052 comment=67d642fc17f1eb08114787d850392d17798d8790 regenmaschine-2024.03.0/000077500000000000000000000000001457362162600146465ustar00rootroot00000000000000regenmaschine-2024.03.0/.codeclimate.yml000066400000000000000000000003401457362162600177150ustar00rootroot00000000000000--- engines: duplication: enabled: true config: languages: - python fixme: enabled: true radon: enabled: true ratings: paths: - "**.py" exclude_paths: - dist/ - docs/ - tests/ regenmaschine-2024.03.0/.flake8000066400000000000000000000002271457362162600160220ustar00rootroot00000000000000[flake8] ignore = E203,E266,E501,F811,W503 max-line-length = 80 max-complexity = 18 per-file-ignores = tests/*:DAR,S101 select = B,B9,LK,C,D,E,F,I,S,W regenmaschine-2024.03.0/.github/000077500000000000000000000000001457362162600162065ustar00rootroot00000000000000regenmaschine-2024.03.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001457362162600203715ustar00rootroot00000000000000regenmaschine-2024.03.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000007641457362162600230720ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. regenmaschine-2024.03.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000006461457362162600241240ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Additional context** Add any other context or screenshots about the feature request here. regenmaschine-2024.03.0/.github/dependabot.yml000066400000000000000000000005521457362162600210400ustar00rootroot00000000000000--- version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily time: "06:00" - package-ecosystem: pip directory: "/.github/workflows" schedule: interval: daily time: "06:00" - package-ecosystem: "github-actions" directory: "/" schedule: interval: daily time: "06:00" regenmaschine-2024.03.0/.github/labels.yml000066400000000000000000000024241457362162600201750ustar00rootroot00000000000000--- - name: "breaking-change" color: ee0701 description: "A breaking change for existing users" - name: "bug" color: ee0701 description: "Bugs or issues which will cause a problem for users" - name: "documentation" color: 0052cc description: "Project documentation" - name: "enhancement" color: 1d76db description: "Enhancement of the code, not introducing new features." - name: "maintenance" color: 2af79e description: "Generic library tasks" - name: "dependencies" color: 1d76db description: "Upgrade or downgrade of project dependencies" - name: "in-progress" color: fbca04 description: "Issue is currently being resolved by a developer" - name: "stale" color: fef2c0 description: "There has not been activity on this issue or PR for some time" - name: "no-stale" color: fef2c0 description: "This issue or PR is exempted from the stale bot" - name: "security" color: ee0701 description: "Marks a security issue that needs to be resolved ASAP" - name: "incomplete" color: fef2c0 description: "Marks a PR or issue that is missing information" - name: "invalid" color: fef2c0 description: "Marks a PR or issue that is missing information" - name: "help-wanted" color: 0e8a16 description: "Needs a helping hang or expertise in order to resolve" regenmaschine-2024.03.0/.github/pull_request_template.md000066400000000000000000000005431457362162600231510ustar00rootroot00000000000000**Describe what the PR does:** **Does this fix a specific issue?** Fixes https://github.com/bachya/regenmaschine/issues/ **Checklist:** - [ ] Confirm that one or more new tests are written for the new functionality. - [ ] Run tests and ensure everything passes (with 100% test coverage). - [ ] Update `README.md` with any new documentation. regenmaschine-2024.03.0/.github/release-drafter.yml000066400000000000000000000010051457362162600217720ustar00rootroot00000000000000--- categories: - title: "🚨 Breaking Changes" labels: - "breaking-change" - title: "🚀 Features" labels: - "enhancement" - title: "🐛 Bug Fixes" labels: - "bug" - title: "📕 Documentation" labels: - "documentation" - title: "🧰 Maintenance" labels: - "dependencies" - "maintenance" - "tooling" change-template: "- $TITLE (#$NUMBER)" name-template: "$NEXT_PATCH_VERSION" tag-template: "$NEXT_PATCH_VERSION" template: | $CHANGES regenmaschine-2024.03.0/.github/workflows/000077500000000000000000000000001457362162600202435ustar00rootroot00000000000000regenmaschine-2024.03.0/.github/workflows/codeql.yml000066400000000000000000000010021457362162600222260ustar00rootroot00000000000000--- name: CodeQL "on": push: branches: - dev - main pull_request: branches: - dev - main workflow_dispatch: schedule: - cron: "30 1 * * 0" jobs: codeql: name: Scanning runs-on: ubuntu-latest steps: - name: ⤵️ Check out code from GitHub uses: actions/checkout@v4 - name: 🏗 Initialize CodeQL uses: github/codeql-action/init@v3 - name: 🚀 Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 regenmaschine-2024.03.0/.github/workflows/labels.yml000066400000000000000000000006721457362162600222350ustar00rootroot00000000000000--- name: Sync Labels "on": push: branches: - main paths: - .github/labels.yml workflow_dispatch: jobs: labels: name: ♻️ Sync labels runs-on: ubuntu-latest steps: - name: ⤵️ Check out code from GitHub uses: actions/checkout@v4 - name: 🚀 Run Label Syncer uses: micnncim/action-label-syncer@v1.3.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} regenmaschine-2024.03.0/.github/workflows/lock.yml000066400000000000000000000006251457362162600217210ustar00rootroot00000000000000--- name: Lock Closed Issues and PRs "on": schedule: - cron: "0 9 * * *" workflow_dispatch: jobs: lock: name: 🔒 Lock! runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v5.0.1 with: github-token: ${{ github.token }} issue-inactive-days: "30" issue-lock-reason: "" pr-inactive-days: "1" pr-lock-reason: "" regenmaschine-2024.03.0/.github/workflows/publish.yml000066400000000000000000000007771457362162600224470ustar00rootroot00000000000000--- name: Publish to PyPI "on": push: tags: - "*" jobs: publish_to_pypi: runs-on: ubuntu-latest steps: - name: ⤵️ Check out code from GitHub uses: actions/checkout@v4 - name: 🏗 Set up Python 3.12 id: python uses: actions/setup-python@v5 with: python-version: "3.12" - name: 🚀 Publish to PyPi run: | pip install poetry poetry publish --build -u __token__ -p ${{ secrets.PYPI_API_KEY }} regenmaschine-2024.03.0/.github/workflows/release-drafter.yml000066400000000000000000000005331457362162600240340ustar00rootroot00000000000000--- name: Release Drafter "on": push: branches: - main workflow_dispatch: jobs: update_release_draft: name: ✏️ Draft Release runs-on: ubuntu-latest steps: - name: 🚀 Run Release Drafter uses: release-drafter/release-drafter@v6.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} regenmaschine-2024.03.0/.github/workflows/requirements.txt000066400000000000000000000000161457362162600235240ustar00rootroot00000000000000poetry==1.8.2 regenmaschine-2024.03.0/.github/workflows/stale.yml000066400000000000000000000023461457362162600221030ustar00rootroot00000000000000--- name: Stale "on": schedule: - cron: "0 8 * * *" workflow_dispatch: jobs: stale: name: 🧹 Clean up stale issues and PRs runs-on: ubuntu-latest steps: - name: 🚀 Run stale uses: actions/stale@v9 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 days-before-close: 7 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no-stale,help-wanted" stale-issue-message: > There hasn't been any activity on this issue recently, so it has been marked as stale. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by leaving a comment. This issue will be closed if no further activity occurs. Thanks! stale-pr-label: "stale" exempt-pr-labels: "no-stale" stale-pr-message: > There hasn't been any activity on this pull request recently, so it has automatically been marked as stale and will be closed if no further action occurs within 7 days. Thank you for your contributions. regenmaschine-2024.03.0/.github/workflows/static-analysis.yml000066400000000000000000000032511457362162600240770ustar00rootroot00000000000000--- name: Linting and Static Analysis "on": pull_request: branches: - dev - main push: branches: - dev - main jobs: lint: name: "Linting & Static Analysis" runs-on: ubuntu-latest steps: - name: ⤵️ Check out code from GitHub uses: actions/checkout@v4 - name: 🏗 Set up Python 3.12 id: setup-python uses: actions/setup-python@v5 with: python-version: "3.12" - name: ⤵️ Get pip cache directory id: pip-cache run: | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: ⤵️ Establish pip cache uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: "${{ runner.os }}-pip-\ ${{ hashFiles('.github/workflows/requirements.txt') }}" restore-keys: | ${{ runner.os }}-pip- - name: 🏗 Install workflow dependencies run: | pip install -r .github/workflows/requirements.txt poetry config virtualenvs.create true poetry config virtualenvs.in-project true - name: ⤵️ Establish poetry cache uses: actions/cache@v4 with: path: .venv key: "venv-${{ steps.setup-python.outputs.python-version }}-\ ${{ hashFiles('poetry.lock') }}" restore-keys: | venv-${{ steps.setup-python.outputs.python-version }}- - name: 🏗 Install package dependencies run: | poetry install --no-interaction - name: 🚀 Run pre-commit hooks uses: pre-commit/action@v3.0.1 env: SKIP: no-commit-to-branch,pytest regenmaschine-2024.03.0/.github/workflows/test.yml000066400000000000000000000072561457362162600217570ustar00rootroot00000000000000--- name: Tests and Coverage "on": pull_request: branches: - dev - main push: branches: - dev - main jobs: test: name: Tests runs-on: ubuntu-latest strategy: matrix: python-version: - "3.10" - "3.11" - "3.12" steps: - name: ⤵️ Check out code from GitHub uses: actions/checkout@v4 - name: 🏗 Set up Python id: setup-python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: ⤵️ Get pip cache directory id: pip-cache run: | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: ⤵️ Establish pip cache uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: "${{ runner.os }}-pip-\ ${{ hashFiles('.github/workflows/requirements.txt') }}" restore-keys: | ${{ runner.os }}-pip- - name: 🏗 Install workflow dependencies run: | pip install -r .github/workflows/requirements.txt poetry config virtualenvs.create true poetry config virtualenvs.in-project true - name: ⤵️ Establish poetry cache uses: actions/cache@v4 with: path: .venv key: "venv-${{ steps.setup-python.outputs.python-version }}-\ ${{ hashFiles('poetry.lock') }}" restore-keys: | venv-${{ steps.setup-python.outputs.python-version }}- - name: 🏗 Install package dependencies run: | poetry install --no-interaction - name: 🚀 Run pytest run: poetry run pytest --cov regenmaschine tests - name: ⬆️ Upload coverage artifact uses: actions/upload-artifact@v3 with: name: coverage-${{ matrix.python-version }} path: .coverage coverage: name: Code Coverage needs: test runs-on: ubuntu-latest steps: - name: ⤵️ Check out code from GitHub uses: actions/checkout@v4 - name: ⬇️ Download coverage data uses: actions/download-artifact@v3 - name: 🏗 Set up Python 3.12 id: setup-python uses: actions/setup-python@v5 with: python-version: "3.12" - name: ⤵️ Get pip cache directory id: pip-cache run: | echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: ⤵️ Establish pip cache uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: "${{ runner.os }}-pip-\ ${{ hashFiles('.github/workflows/requirements.txt') }}" restore-keys: | ${{ runner.os }}-pip- - name: 🏗 Install workflow dependencies run: | pip install -r .github/workflows/requirements.txt poetry config virtualenvs.create true poetry config virtualenvs.in-project true - name: ⤵️ Establish poetry cache uses: actions/cache@v4 with: path: .venv key: "venv-${{ steps.setup-python.outputs.python-version }}-\ ${{ hashFiles('poetry.lock') }}" restore-keys: | venv-${{ steps.setup-python.outputs.python-version }}- - name: 🏗 Install package dependencies run: | poetry install --no-interaction - name: 🚀 Process coverage results run: | poetry run coverage combine coverage*/.coverage* poetry run coverage xml -i - name: 📊 Upload coverage report to codecov.io uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} regenmaschine-2024.03.0/.gitignore000066400000000000000000000001451457362162600166360ustar00rootroot00000000000000*.egg-info .DS_Store .coverage .mypy_cache .nox .tox .venv __pycache__ coverage.xml docs/_build tags regenmaschine-2024.03.0/.mise.toml000066400000000000000000000000701457362162600165530ustar00rootroot00000000000000[tools] python = { version="3.12", virtualenv=".venv" } regenmaschine-2024.03.0/.pre-commit-config.yaml000066400000000000000000000115051457362162600211310ustar00rootroot00000000000000--- repos: - repo: local hooks: - id: blacken-docs name: ☕️ Format documentation using black language: system files: '\.(rst|md|markdown|py|tex)$' entry: poetry run blacken-docs require_serial: true - id: check-ast name: 🐍 Checking Python AST language: system types: [python] entry: poetry run check-ast - id: check-case-conflict name: 🔠 Checking for case conflicts language: system entry: poetry run check-case-conflict - id: check-docstring-first name: ℹ️ Checking docstrings are first language: system types: [python] entry: poetry run check-docstring-first - id: check-executables-have-shebangs name: 🧐 Checking that executables have shebangs language: system types: [text, executable] entry: poetry run check-executables-have-shebangs stages: [commit, push, manual] - id: check-json name: { Checking JSON files language: system types: [json] entry: poetry run check-json - id: check-merge-conflict name: 💥 Checking for merge conflicts language: system types: [text] entry: poetry run check-merge-conflict - id: check-symlinks name: 🔗 Checking for broken symlinks language: system types: [symlink] entry: poetry run check-symlinks - id: check-toml name: ✅ Checking TOML files language: system types: [toml] entry: poetry run check-toml - id: check-yaml name: ✅ Checking YAML files language: system types: [yaml] entry: poetry run check-yaml - id: codespell name: ✅ Checking code for misspellings language: system types: [text] exclude: ^poetry\.lock$ entry: poetry run codespell - id: debug-statements name: 🪵 Checking for debug statements and imports (Python) language: system types: [python] entry: poetry run debug-statement-hook - id: detect-private-key name: 🕵️ Detecting private keys language: system types: [text] entry: poetry run detect-private-key - id: end-of-file-fixer name: 🔚 Checking end of files language: system types: [text] entry: poetry run end-of-file-fixer stages: [commit, push, manual] - id: fix-byte-order-marker name: 🚏 Checking UTF-8 byte order marker language: system types: [text] entry: poetry run fix-byte-order-marker - id: format name: ☕️ Formatting code using ruff language: system types: [python] entry: poetry run ruff format - id: isort name: 🔀 Sorting all imports with isort language: system types: [python] entry: poetry run isort - id: mypy name: 🆎 Performing static type checking using mypy language: system types: [python] entry: poetry run mypy require_serial: true - id: no-commit-to-branch name: 🛑 Checking for commit to protected branch language: system entry: poetry run no-commit-to-branch pass_filenames: false always_run: true args: - --branch=dev - --branch=main - id: poetry name: 📜 Checking pyproject with Poetry language: system entry: poetry check pass_filenames: false always_run: true - id: pylint name: 🌟 Starring code with pylint language: system types: [python] entry: poetry run pylint - id: pyupgrade name: 🆙 Checking for upgradable syntax with pyupgrade language: system types: [python] entry: poetry run pyupgrade args: [--py39-plus, --keep-runtime-typing] - id: ruff name: 👔 Enforcing style guide with ruff language: system types: [python] entry: poetry run ruff --fix - id: trailing-whitespace name: ✄ Trimming trailing whitespace language: system types: [text] entry: poetry run trailing-whitespace-fixer stages: [commit, push, manual] - id: vulture name: 🔍 Finding unused Python code with Vulture language: system types: [python] entry: poetry run vulture pass_filenames: false require_serial: true - id: yamllint name: 🎗 Checking YAML files with yamllint language: system types: [yaml] entry: poetry run yamllint - repo: https://github.com/pre-commit/mirrors-prettier rev: "v3.0.0-alpha.4" hooks: - id: prettier name: 💄 Ensuring files are prettier regenmaschine-2024.03.0/LICENSE000066400000000000000000000020721457362162600156540ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2017-2024 Aaron Bach Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. regenmaschine-2024.03.0/README.md000066400000000000000000000334571457362162600161410ustar00rootroot00000000000000# 💧 Regenmaschine: A Simple Python Library for RainMachine™ [![CI][ci-badge]][ci] [![PyPI][pypi-badge]][pypi] [![Version][version-badge]][version] [![License][license-badge]][license] [![Code Coverage][codecov-badge]][codecov] [![Maintainability][maintainability-badge]][maintainability] Buy Me A Coffee `regenmaschine` (German for "rain machine") is a simple, clean, well-tested Python library for interacting with [RainMachine™ smart sprinkler controllers][regenmaschine]. It gives developers an easy API to manage their controllers over their local LAN or remotely via the RainMachine™ cloud. - [Remote Access Announcement](#remote-access-announcement-2022-06-26) - [Python Versions](#python-versions) - [Installation](#installation) - [Usage](#usage) - [Loading Controllers Multiple Times](#loading-controllers-multiple-times) - [Contributing](#contributing) # Remote Access Announcement (2022-06-26) On June 2, 2022, RainMachine announced a [Premium Services][rainmachine-premium] addition; under this new model, remote access is _only_ available to subscribers of these Premium Services. I do not currently intend to subscribe to Premium Services; as such, the remote access abilities of `regenmaschine` will remain as-is from here on out unless spurred on by others. They may stop working at any time. PRs from subscribing users are always welcome. # Python Versions `regenmaschine` is currently supported on: - Python 3.10 - Python 3.11 - Python 3.12 # Installation ```bash pip install regenmaschine ``` # Usage Creating a `regenmaschine` `Client` might be the easiest thing you do all day: ```python import asyncio from aiohttp import ClientSession from regenmaschine import Client async def main() -> None: """Run!""" client = Client() # ... asyncio.run(main()) ``` By default, the library creates a new connection to the sprinkler controller with each coroutine. If you are calling a large number of coroutines (or merely want to squeeze out every second of runtime savings possible), an [`aiohttp`][aiohttp] `ClientSession` can be used for connection pooling: See the module docstrings throughout the library for full info on all parameters, return types, etc. ```python import asyncio from aiohttp import ClientSession from regenmaschine import Client async def main() -> None: """Run!""" async with ClientSession() as session: client = Client(session=session) asyncio.run(main()) ``` ## Loading Local (Accessible Over the LAN) Controllers Once you have a client, you can load a local controller (i.e., one that is accessible over the LAN) very easily: ```python import asyncio from aiohttp import ClientSession from regenmaschine import Client async def main() -> None: """Run!""" async with ClientSession() as session: client = Client(session=session) await client.load_local("192.168.1.101", "my_password", port=8080, use_ssl=True) controllers = client.controllers # >>> {'ab:cd:ef:12:34:56': } asyncio.run(main()) ``` ## Loading Remote (Accessible Over the RainMachine Cloud) Controllers If you have 1, 2 or 100 other local controllers, you can load them in the same way – `client.controllers` will keep your controllers all organized. What if you have controllers around the world and can't access them all over the same local network? No problem! `regenmaschine` allows you to load remote controllers very easily, as well: ```python import asyncio from aiohttp import ClientSession from regenmaschine import Client async def main() -> None: """Run!""" async with ClientSession() as session: client = Client(session=session) await client.load_remote("rainmachine_email@host.com", "my_password") controllers = client.controllers # >>> {'xx:xx:xx:xx:xx:xx': , ...} asyncio.run(main()) ``` Bonus tip: `client.load_remote` will load _all_ controllers owned by that email address. ## Using the Controller Regardless of the type of controller you have loaded (local or remote), the same properties and methods are available to each: ```python import asyncio import datetime from aiohttp import ClientSession from regenmaschine import Client async def main() -> None: """Run!""" async with ClientSession() as session: client = Client(session=session) # Load a local controller: await client.load_local("192.168.1.101", "my_password", port=8080, use_ssl=True) # Load all remote controllers associated with an account: await client.load_remote("rainmachine_email@host.com", "my_password") # They all act the same! The only difference is that remote API calls # will pass through the RainMachine™ cloud: for mac_address, controller in client.controllers: # Print some client properties: print(f"Name: {controller.name}") print(f"Host: {controller.host}") print(f"MAC Address: {controller.mac}") print(f"API Version: {controller.api_version}") print(f"Software Version: {controller.software_version}") print(f"Hardware Version: {controller.hardware_version}") # Get all diagnostic information: diagnostics = await controller.diagnostics.current() # Get all weather parsers: parsers = await controller.parsers.current() # Get all programs: programs = await controller.programs.all() # Include inactive programs: programs = await controller.programs.all(include_inactive=True) # Get a specific program: program_1 = await controller.programs.get(1) # Enable or disable a specific program: await controller.programs.enable(1) await controller.programs.disable(1) # Get the next run time for all programs: runs = await controller.programs.next() # Get all running programs: programs = await controller.programs.running() # Start and stop a program: await controller.programs.start(1) await controller.programs.stop(1) # Get basic details about all zones: zones = await controller.zones.all() # Get advanced details about all zones: zones = await controller.zones.all(details=True) # Include inactive zones: zones = await controller.zones.all(include_inactive=True) # Get basic details about a specific zone: zone_1 = await controller.zones.get(1) # Get advanced details about a specific zone: zone_1 = await controller.zones.get(1, details=True) # Enable or disable a specific zone: await controller.zones.enable(1) await controller.zones.disable(1) # Start a zone for 60 seconds: await controller.zones.start(1, 60) # ...and stop it: await controller.zones.stop(1) # Get all running zones: programs = await controller.zones.running() # Get the device name: name = await controller.provisioning.device_name # Get all provisioning settings: settings = await controller.provisioning.settings() # Get all networking info related to the device: wifi = await controller.provisioning.wifi() # Get various types of active watering restrictions: current = await controller.restrictions.current() universal = await controller.restrictions.universal() hourly = await controller.restrictions.hourly() raindelay = await controller.restrictions.raindelay() # Set universal restrictions – note that the payload is the same structure # as returned by controller.restrictions.universal(): await controller.restrictions.set_universal( { "hotDaysExtraWatering": False, "freezeProtectEnabled": True, } ) # Get watering stats: today = await controller.stats.on_date(datetime.date.today()) upcoming_days = await controller.stats.upcoming(details=True) # Get info on various watering activities not already covered: log = await controller.watering.log(datetime.date.today(), 2) queue = await controller.watering.queue() runs = await controller.watering.runs(datetime.date.today()) # Pause all watering activities for 30 seconds: await controller.watering.pause_all(30) # Unpause all watering activities: await controller.watering.unpause_all() # Stop all watering activities: await controller.watering.stop_all() # See if a firmware update is available: update_data = await controller.machine.get_firmware_update_status() # ...and request the update: update_data = await controller.machine.update_firmware() # Reboot the controller: update_data = await controller.machine.reboot() # Return the current flow meter data: flowmeter = await controller.watering.flowmeter() # Add values to flowmeter counters from an external smart water meter # not wired directly to the controller. # Units can be "clicks", "gal", "m3" and "litre". await controller.watering.post_flowmeter({"value": 2000, "units": "clicks"}) asyncio.run(main()) ``` Check out `example.py`, the tests, and the source files themselves for method signatures and more examples. For additional reference, the full RainMachine™ API documentation is available [here][rainmachine-api]. # Loading Controllers Multiple Times It is technically possible to load a controller multiple times. Let's pretend for a moment that: - We have a local controller named `Home` (available at `192.168.1.101`). - We have a remote controller named `Grandma's House`. - Both controllers live under our email address: `user@host.com` If we load them thus: ```python import asyncio from aiohttp import ClientSession from regenmaschine import Client async def main() -> None: """Run!""" async with ClientSession() as session: client = Client(session=session) # Load "Home" locally: await client.load_local("192.168.1.101", "my_password") # Load all of my controllers remotely: await client.load_remote("user@host.com", "my_password") asyncio.run(main()) ``` ...then we will have the following: 1. `Home` will be a `LocalController` and accessible over the LAN. 2. `Grandma's House` will be a `RemoteController` and accessible only over the RainMachine™ cloud. Notice that `regenmaschine` is smart enough to not overwrite a controller that already exists: even though `Home` exists as a remote controller owned by `user@host.com`, it had already been loaded locally. By default, `regenmaschine` will only load a controller if it hasn't been loaded before (locally _or_ remotely). If you want to change this behavior, both `load_local` and `load_remote` accept an optional `skip_existing` parameter: ```python import asyncio from aiohttp import ClientSession from regenmaschine import Client async def main() -> None: """Run!""" async with ClientSession() as session: client = Client(session=session) # Load all of my controllers remotely: await client.load_remote("user@host.com", "my_password") # Load "Home" locally, overwriting the existing remote controller: await client.load_local("192.168.1.101", "my_password", skip_existing=False) asyncio.run(main()) ``` # Contributing Thanks to all of [our contributors][contributors] so far! 1. [Check for open features/bugs][issues] or [initiate a discussion on one][new-issue]. 2. [Fork the repository][fork]. 3. (_optional, but highly recommended_) Create a virtual environment: `python3 -m venv .venv` 4. (_optional, but highly recommended_) Enter the virtual environment: `source ./.venv/bin/activate` 5. Install the dev environment: `script/setup` 6. Code your new feature or bug fix on a new branch. 7. Write tests that cover your new functionality. 8. Run tests and ensure 100% code coverage: `poetry run pytest --cov regenmaschine tests` 9. Update `README.md` with any new documentation. 10. Submit a pull request! [aiohttp]: https://github.com/aio-libs/aiohttp [ci-badge]: https://img.shields.io/github/actions/workflow/status/bachya/regenmaschine/test.yml [ci]: https://github.com/bachya/regenmaschine/actions [codecov-badge]: https://codecov.io/gh/bachya/regenmaschine/branch/dev/graph/badge.svg [codecov]: https://codecov.io/gh/bachya/regenmaschine [contributors]: https://github.com/bachya/regenmaschine/graphs/contributors [fork]: https://github.com/bachya/regenmaschine/fork [issues]: https://github.com/bachya/regenmaschine/issues [license-badge]: https://img.shields.io/pypi/l/regenmaschine.svg [license]: https://github.com/bachya/regenmaschine/blob/main/LICENSE [maintainability-badge]: https://api.codeclimate.com/v1/badges/cb14e60d5f5a4c2ccb2c/maintainability [maintainability]: https://codeclimate.com/github/bachya/regenmaschine/maintainability [new-issue]: https://github.com/bachya/regenmaschine/issues/new [pypi-badge]: https://img.shields.io/pypi/v/regenmaschine.svg [pypi]: https://pypi.python.org/pypi/regenmaschine [rainmachine-api]: https://rainmachine.docs.apiary.io/ [rainmachine-premium]: https://www.rainmachine.com/premium/ [regenmaschine]: http://www.rainmachine.com/ [version-badge]: https://img.shields.io/pypi/pyversions/regenmaschine.svg [version]: https://pypi.python.org/pypi/regenmaschine regenmaschine-2024.03.0/examples/000077500000000000000000000000001457362162600164645ustar00rootroot00000000000000regenmaschine-2024.03.0/examples/__init__.py000066400000000000000000000000271457362162600205740ustar00rootroot00000000000000"""Define examples.""" regenmaschine-2024.03.0/examples/test_api.py000066400000000000000000000073321457362162600206530ustar00rootroot00000000000000"""Run an example script to quickly test.""" # pylint: disable=too-many-locals,too-many-statements import asyncio import datetime import logging from aiohttp import ClientSession from regenmaschine import Client from regenmaschine.errors import RainMachineError IP_ADDRESS = "" PASSWORD = "" # noqa: S105 _LOGGER = logging.getLogger(__name__) async def main() -> None: """Run.""" logging.basicConfig(level=logging.INFO) async with ClientSession() as session: try: client = Client(session=session) await client.load_local(IP_ADDRESS, PASSWORD) for controller in client.controllers.values(): _LOGGER.info("CLIENT INFORMATION") _LOGGER.info("Name: %s", controller.name) _LOGGER.info("MAC Address: %s", controller.mac) _LOGGER.info("API Version: %s", controller.api_version) _LOGGER.info("Software Version: %s", controller.software_version) _LOGGER.info("Hardware Version: %s", controller.hardware_version) _LOGGER.info("RAINMACHINE DIAGNOSTICS") diagnostics = await controller.diagnostics.current() _LOGGER.info(diagnostics) _LOGGER.info("RAINMACHINE PARSERS") parsers = await controller.parsers.current() _LOGGER.info(parsers) # Work with programs: _LOGGER.info("ALL PROGRAMS") programs = await controller.programs.all(include_inactive=True) _LOGGER.info(programs) _LOGGER.info("NEXT RUN TIMES") next_programs = await controller.programs.next() _LOGGER.info(next_programs) _LOGGER.info("RUNNING PROGRAMS") running_programs = await controller.programs.running() _LOGGER.info(running_programs) _LOGGER.info("PROVISIONING INFO") name = await controller.provisioning.device_name _LOGGER.info("Device Name: %s", name) settings = await controller.provisioning.settings() _LOGGER.info(settings) wifi = await controller.provisioning.wifi() _LOGGER.info(wifi) _LOGGER.info("RESTRICTIONS") current = await controller.restrictions.current() _LOGGER.info(current) universal = await controller.restrictions.universal() _LOGGER.info(universal) hourly = await controller.restrictions.hourly() _LOGGER.info(hourly) raindelay = await controller.restrictions.raindelay() _LOGGER.info(raindelay) _LOGGER.info("STATS") today = await controller.stats.on_date(date=datetime.date.today()) _LOGGER.info(today) upcoming = await controller.stats.upcoming(details=True) _LOGGER.info(upcoming) _LOGGER.info("WATERING") log = await controller.watering.log(date=datetime.date.today()) _LOGGER.info(log) queue = await controller.watering.queue() _LOGGER.info(queue) runs = await controller.watering.runs(date=datetime.date.today()) _LOGGER.info(runs) _LOGGER.info("ALL ACTIVE ZONES") zones = await controller.zones.all(details=True) _LOGGER.info(zones) _LOGGER.info("FLOW METER") flowmeter = await controller.watering.flowmeter() _LOGGER.info(flowmeter) except RainMachineError as err: print(err) asyncio.run(main()) regenmaschine-2024.03.0/poetry.lock000066400000000000000000003247241457362162600170560ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" version = "3.9.3" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54"}, {file = "aiohttp-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc"}, {file = "aiohttp-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5"}, {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b"}, {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768"}, {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5"}, {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29"}, {file = "aiohttp-3.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec"}, {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747"}, {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6"}, {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c"}, {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf"}, {file = "aiohttp-3.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52"}, {file = "aiohttp-3.9.3-cp310-cp310-win32.whl", hash = "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b"}, {file = "aiohttp-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5"}, {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d"}, {file = "aiohttp-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2"}, {file = "aiohttp-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc"}, {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266"}, {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce"}, {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb"}, {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b"}, {file = "aiohttp-3.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4"}, {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae"}, {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3"}, {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283"}, {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e"}, {file = "aiohttp-3.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4"}, {file = "aiohttp-3.9.3-cp311-cp311-win32.whl", hash = "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5"}, {file = "aiohttp-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8"}, {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60"}, {file = "aiohttp-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869"}, {file = "aiohttp-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679"}, {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96"}, {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d"}, {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11"}, {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53"}, {file = "aiohttp-3.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5"}, {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca"}, {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1"}, {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5"}, {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6"}, {file = "aiohttp-3.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f"}, {file = "aiohttp-3.9.3-cp312-cp312-win32.whl", hash = "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38"}, {file = "aiohttp-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5"}, {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c"}, {file = "aiohttp-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528"}, {file = "aiohttp-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542"}, {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511"}, {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672"}, {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d"}, {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168"}, {file = "aiohttp-3.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b"}, {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194"}, {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8"}, {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4"}, {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321"}, {file = "aiohttp-3.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2"}, {file = "aiohttp-3.9.3-cp38-cp38-win32.whl", hash = "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63"}, {file = "aiohttp-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c"}, {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29"}, {file = "aiohttp-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1"}, {file = "aiohttp-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f"}, {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3"}, {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa"}, {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e"}, {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58"}, {file = "aiohttp-3.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533"}, {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb"}, {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3"}, {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d"}, {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a"}, {file = "aiohttp-3.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d"}, {file = "aiohttp-3.9.3-cp39-cp39-win32.whl", hash = "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051"}, {file = "aiohttp-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc"}, {file = "aiohttp-3.9.3.tar.gz", hash = "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7"}, ] [package.dependencies] aiosignal = ">=1.1.2" async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" [package.extras] speedups = ["Brotli", "aiodns", "brotlicffi"] [[package]] name = "aiosignal" version = "1.2.0" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.6" files = [ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, ] [package.dependencies] frozenlist = ">=1.1.0" [[package]] name = "aresponses" version = "3.0.0" description = "Asyncio response mocking. Similar to the responses library used for 'requests'" optional = false python-versions = ">=3.7" files = [ {file = "aresponses-3.0.0-py3-none-any.whl", hash = "sha256:8093ab4758eb4aba91c765a50295b269ecfc0a9e7c7158954760bc0c23503970"}, {file = "aresponses-3.0.0.tar.gz", hash = "sha256:8731d0609fe4c954e21f17753dc868dca9e2e002b020a33dc9212004599b11e7"}, ] [package.dependencies] aiohttp = [ {version = ">=3.7.0", markers = "python_version >= \"3.10\" and python_version < \"3.12\""}, {version = ">=3.7.0,<3.8.dev0 || >=3.9.dev0", markers = "python_version >= \"3.12\""}, ] pytest-asyncio = {version = ">=0.17.0", markers = "python_version >= \"3.7\""} [[package]] name = "astroid" version = "3.1.0" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ {file = "astroid-3.1.0-py3-none-any.whl", hash = "sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819"}, {file = "astroid-3.1.0.tar.gz", hash = "sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"}, ] [package.dependencies] typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "async-timeout" version = "4.0.2" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.6" files = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] [[package]] name = "asynctest" version = "0.13.0" description = "Enhance the standard unittest package with features for testing asyncio libraries" optional = false python-versions = ">=3.5" files = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, ] [[package]] name = "attrs" version = "22.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.5" files = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] [package.extras] dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "black" version = "23.11.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "blacken-docs" version = "1.16.0" description = "Run Black on Python code blocks in documentation files." optional = false python-versions = ">=3.8" files = [ {file = "blacken_docs-1.16.0-py3-none-any.whl", hash = "sha256:b0dcb84b28ebfb352a2539202d396f50e15a54211e204a8005798f1d1edb7df8"}, {file = "blacken_docs-1.16.0.tar.gz", hash = "sha256:b4bdc3f3d73898dfbf0166f292c6ccfe343e65fc22ddef5319c95d1a8dcc6c1c"}, ] [package.dependencies] black = ">=22.1.0" [[package]] name = "certifi" version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.6.1" files = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] [[package]] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "codespell" version = "2.2.6" description = "Codespell" optional = false python-versions = ">=3.8" files = [ {file = "codespell-2.2.6-py3-none-any.whl", hash = "sha256:9ee9a3e5df0990604013ac2a9f22fa8e57669c827124a2e961fe8a1da4cacc07"}, {file = "codespell-2.2.6.tar.gz", hash = "sha256:a8c65d8eb3faa03deabab6b3bbe798bea72e1799c7e9e955d57eca4096abcff9"}, ] [package.extras] dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"] hard-encoding-detection = ["chardet"] toml = ["tomli"] types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] [[package]] name = "coverage" version = "7.4.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "darglint" version = "1.8.1" description = "A utility for ensuring Google-style docstrings stay up to date with the source code." optional = false python-versions = ">=3.6,<4.0" files = [ {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, ] [[package]] name = "dill" version = "0.3.7" description = "serialize all of Python" optional = false python-versions = ">=3.7" files = [ {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, ] [package.extras] graph = ["objgraph (>=1.7.2)"] [[package]] name = "distlib" version = "0.3.6" description = "Distribution utilities" optional = false python-versions = "*" files = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] [[package]] name = "exceptiongroup" version = "1.0.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"}, {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" version = "3.8.0" description = "A platform independent file lock." optional = false python-versions = ">=3.7" files = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] [package.extras] docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] [[package]] name = "frozenlist" version = "1.4.1" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false python-versions = ">=3.8" files = [ {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] [[package]] name = "gitdb" version = "4.0.9" description = "Git Object Database" optional = false python-versions = ">=3.6" files = [ {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, ] [package.dependencies] smmap = ">=3.0.1,<6" [[package]] name = "gitpython" version = "3.1.42" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ {file = "GitPython-3.1.42-py3-none-any.whl", hash = "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd"}, {file = "GitPython-3.1.42.tar.gz", hash = "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar"] [[package]] name = "identify" version = "2.5.8" description = "File identification library for Python" optional = false python-versions = ">=3.7" files = [ {file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"}, {file = "identify-2.5.8.tar.gz", hash = "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" optional = false python-versions = "*" files = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] [[package]] name = "isort" version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] [package.extras] colors = ["colorama (>=0.4.6)"] [[package]] name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" optional = false python-versions = "*" files = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] [[package]] name = "multidict" version = "6.0.2" description = "multidict implementation" optional = false python-versions = ">=3.7" files = [ {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, ] [[package]] name = "mypy" version = "1.9.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "nodeenv" version = "1.7.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, ] [package.dependencies] setuptools = "*" [[package]] name = "packaging" version = "23.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] [[package]] name = "pathspec" version = "0.10.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.7" files = [ {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, ] [[package]] name = "platformdirs" version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] [package.extras] docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" version = "3.6.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "pre-commit-hooks" version = "4.5.0" description = "Some out-of-the-box hooks for pre-commit." optional = false python-versions = ">=3.8" files = [ {file = "pre_commit_hooks-4.5.0-py2.py3-none-any.whl", hash = "sha256:b779d5c44ede9b1fda48e2d96b08e9aa5b1d2fdb8903ca09f0dbaca22d529edb"}, {file = "pre_commit_hooks-4.5.0.tar.gz", hash = "sha256:ffbe2af1c85ac9a7695866955680b4dee98822638b748a6f3debefad79748c8a"}, ] [package.dependencies] "ruamel.yaml" = ">=0.15" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [[package]] name = "pygments" version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" version = "3.1.0" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ {file = "pylint-3.1.0-py3-none-any.whl", hash = "sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74"}, {file = "pylint-3.1.0.tar.gz", hash = "sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"}, ] [package.dependencies] astroid = ">=3.1.0,<=3.2.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] [[package]] name = "pytest" version = "8.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.4,<2.0" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-aiohttp" version = "1.0.5" description = "Pytest plugin for aiohttp support" optional = false python-versions = ">=3.7" files = [ {file = "pytest-aiohttp-1.0.5.tar.gz", hash = "sha256:880262bc5951e934463b15e3af8bb298f11f7d4d3ebac970aab425aff10a780a"}, {file = "pytest_aiohttp-1.0.5-py3-none-any.whl", hash = "sha256:63a5360fd2f34dda4ab8e6baee4c5f5be4cd186a403cabd498fced82ac9c561e"}, ] [package.dependencies] aiohttp = ">=3.8.1" pytest = ">=6.1.0" pytest-asyncio = ">=0.17.2" [package.extras] testing = ["coverage (==6.2)", "mypy (==0.931)"] [[package]] name = "pytest-asyncio" version = "0.23.5.post1" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ {file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"}, {file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"}, ] [package.dependencies] pytest = ">=7.0.0,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" files = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-mock" version = "3.12.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" files = [ {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, ] [package.dependencies] pytest = ">=5.0" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pyupgrade" version = "3.15.1" description = "A tool to automatically upgrade syntax for newer versions." optional = false python-versions = ">=3.8.1" files = [ {file = "pyupgrade-3.15.1-py2.py3-none-any.whl", hash = "sha256:c5e005de2805edcd333d1deb04553200ec69da85e4bc9db37b16345ed9e27ed9"}, {file = "pyupgrade-3.15.1.tar.gz", hash = "sha256:7690857cae0f6253f39241dcd2e57118c333c438b78609fc3c17a5aa61227b7d"}, ] [package.dependencies] tokenize-rt = ">=5.2.0" [[package]] name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] name = "ruamel-yaml" version = "0.17.21" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false python-versions = ">=3" files = [ {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, ] [package.dependencies] "ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} [package.extras] docs = ["ryd"] jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] [[package]] name = "ruamel-yaml-clib" version = "0.2.7" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" optional = false python-versions = ">=3.5" files = [ {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"}, {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"}, {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"}, {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"}, {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"}, {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"}, {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"}, {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"}, {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"}, {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"}, {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"}, {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"}, {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"}, {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"}, {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"}, {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"}, {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"}, {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"}, {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"}, {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, ] [[package]] name = "ruff" version = "0.3.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"}, {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"}, {file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"}, {file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"}, {file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"}, {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"}, {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"}, {file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"}, {file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"}, {file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"}, {file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"}, {file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"}, {file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"}, {file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"}, {file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"}, {file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"}, {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, ] [[package]] name = "setuptools" version = "65.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.7" files = [ {file = "setuptools-65.5.1-py3-none-any.whl", hash = "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31"}, {file = "setuptools-65.5.1.tar.gz", hash = "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "smmap" version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" optional = false python-versions = ">=3.6" files = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] [[package]] name = "tokenize-rt" version = "5.2.0" description = "A wrapper around the stdlib `tokenize` which roundtrips." optional = false python-versions = ">=3.8" files = [ {file = "tokenize_rt-5.2.0-py2.py3-none-any.whl", hash = "sha256:b79d41a65cfec71285433511b50271b05da3584a1da144a0752e9c621a285289"}, {file = "tokenize_rt-5.2.0.tar.gz", hash = "sha256:9fe80f8a5c1edad2d3ede0f37481cc0cc1538a2f442c9c2f9e4feacd2792d054"}, ] [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] [[package]] name = "tomlkit" version = "0.11.6" description = "Style preserving TOML library" optional = false python-versions = ">=3.6" files = [ {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, ] [[package]] name = "typing-extensions" version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] name = "virtualenv" version = "20.16.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.6" files = [ {file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"}, {file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"}, ] [package.dependencies] distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" platformdirs = ">=2.4,<3" [package.extras] docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] name = "vulture" version = "2.11" description = "Find dead code" optional = false python-versions = ">=3.8" files = [ {file = "vulture-2.11-py2.py3-none-any.whl", hash = "sha256:12d745f7710ffbf6aeb8279ba9068a24d4e52e8ed333b8b044035c9d6b823aba"}, {file = "vulture-2.11.tar.gz", hash = "sha256:f0fbb60bce6511aad87ee0736c502456737490a82d919a44e6d92262cb35f1c2"}, ] [package.dependencies] tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [[package]] name = "yamllint" version = "1.35.1" description = "A linter for YAML files." optional = false python-versions = ">=3.8" files = [ {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"}, {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"}, ] [package.dependencies] pathspec = ">=0.5.3" pyyaml = "*" [package.extras] dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] [[package]] name = "yarl" version = "1.9.4" description = "Yet another URL library" optional = false python-versions = ">=3.7" files = [ {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" content-hash = "fce9e86d1c774126029dbf2d6c19e8379ad4dd47ad6d49cabb14e7b7c3d6ce6b" regenmaschine-2024.03.0/pyproject.toml000066400000000000000000000071731457362162600175720ustar00rootroot00000000000000[build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.black] target-version = ["py39"] [tool.coverage.report] exclude_lines = ["TYPE_CHECKING", "AssertionError", "NotImplementedError", "# pragma: no cover"] fail_under = 100 show_missing = true [tool.coverage.run] source = ["regenmaschine"] [tool.isort] known_first_party = "regenmaschine,examples,tests" multi_line_output = 3 profile = "black" [tool.mypy] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true follow_imports = "silent" ignore_missing_imports = true no_implicit_optional = true platform = "linux" python_version = "3.10" show_error_codes = true strict_equality = true warn_incomplete_stub = true warn_redundant_casts = true warn_return_any = true warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true [tool.poetry] name = "regenmaschine" version = "2024.03.0" description = "A simple API for RainMachine sprinkler controllers" readme = "README.md" authors = ["Aaron Bach "] license = "MIT" repository = "https://github.com/bachya/regenmaschine" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", "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", ] [tool.poetry.dependencies] aiohttp = ">=3.8.0" certifi = ">=2023.07.22" frozenlist = "^1.4.0" python = "^3.10" typing-extensions = "^4.3.0" yarl = ">=1.9.2" [tool.poetry.group.dev.dependencies] GitPython = ">=3.1.35" Pygments = ">=2.15.0" aresponses = ">=2.1.6,<4.0.0" asynctest = "^0.13.0" blacken-docs = "^1.12.1" codespell = "^2.2.2" coverage = {version = ">=6.5,<8.0", extras = ["toml"]} darglint = "^1.8.1" isort = "^5.10.1" mypy = "^1.2.0" pre-commit = ">=2.20,<4.0" pre-commit-hooks = "^4.3.0" pylint = ">=2.15.5,<4.0.0" pytest = ">=7.2,<9.0" pytest-aiohttp = "^1.0.0" pytest-asyncio = ">=0.20.1,<0.24.0" pytest-cov = "^4.0.0" pytest-mock = "^3.0.0" pyupgrade = "^3.1.0" pyyaml = "^6.0.1" ruff = ">=0.0.261,<0.3.3" vulture = "^2.6" yamllint = "^1.28.0" [tool.poetry.urls] "Bug Tracker" = "https://github.com/bachya/regenmaschine/issues" Changelog = "https://github.com/bachya/regenmaschine/releases" [tool.pylint.BASIC] expected-line-ending-format = "LF" [tool.pylint.DESIGN] max-attributes = 20 [tool.pylint.FORMAT] max-line-length = 88 [tool.pylint.MASTER] ignore = [ "tests", ] load-plugins = [ "pylint.extensions.bad_builtin", "pylint.extensions.code_style", "pylint.extensions.docparams", "pylint.extensions.docstyle", "pylint.extensions.empty_comment", "pylint.extensions.overlapping_exceptions", "pylint.extensions.typing", ] [tool.pylint."MESSAGES CONTROL"] # Reasons disabled: # unnecessary-pass - This can hurt readability disable = [ "unnecessary-pass" ] [tool.pylint.REPORTS] score = false [tool.pylint.SIMILARITIES] # Minimum lines number of a similarity. # We set this higher because of some cases where V2 and V3 functionality are # similar, but abstracting them isn't feasible. min-similarity-lines = 8 # Ignore comments when computing similarities. ignore-comments = true # Ignore docstrings when computing similarities. ignore-docstrings = true # Ignore imports when computing similarities. ignore-imports = true [tool.vulture] min_confidence = 80 paths = ["regenmaschine", "tests"] verbose = false regenmaschine-2024.03.0/regenmaschine/000077500000000000000000000000001457362162600174565ustar00rootroot00000000000000regenmaschine-2024.03.0/regenmaschine/__init__.py000066400000000000000000000000661457362162600215710ustar00rootroot00000000000000"""Initialize.""" from .client import Client # noqa regenmaschine-2024.03.0/regenmaschine/client.py000066400000000000000000000217031457362162600213110ustar00rootroot00000000000000"""Define a client to interact with a RainMachine unit.""" from __future__ import annotations import asyncio import json import ssl from datetime import datetime from typing import Any, cast from aiohttp import ClientSession, ClientTimeout from aiohttp.client_exceptions import ClientOSError, ServerDisconnectedError from yarl import URL from .const import LOGGER from .controller import Controller, LocalController, RemoteController from .errors import RequestError, TokenExpiredError, raise_for_error DEFAULT_LOCAL_PORT = 8080 DEFAULT_TIMEOUT = 30 class Client: """Define the client.""" def __init__( self, *, request_timeout: int = DEFAULT_TIMEOUT, session: ClientSession | None = None, ) -> None: """Initialize. Args: request_timeout: The number of seconds before a request times out. session: An optional aiohttp ClientSession. """ self._request_timeout = request_timeout self._session = session self._ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # The local API on Gen 1 controllers uses outdated RSA ciphers (and there isn't # any indication that they'll be updated). Python 3.10+ enforces minimum TLS # standards that the Gen 1 can't support, so to keep compatibility, we loosen # things up: # 1. We set a minimum TLS version of SSLv3 # 2. We utilize the "DEFAULT" cipher suite (which includes old RSA ciphers). # 3. We don't validate the hostname. # 4. We allow self-signed certificates. # 5. We allow legacy server connections. self._ssl_context.minimum_version = ssl.TLSVersion.SSLv3 self._ssl_context.set_ciphers("DEFAULT:@SECLEVEL=0") self._ssl_context.check_hostname = False self._ssl_context.verify_mode = ssl.CERT_NONE self._ssl_context.options |= getattr(ssl, "OP_LEGACY_SERVER_CONNECT", 0x4) self.controllers: dict[str, Controller] = {} async def _request( # pylint: disable=too-many-arguments self, method: str, url: URL, *, access_token: str | None = None, access_token_expiration: datetime | None = None, use_ssl: bool = True, **kwargs: dict[str, Any], ) -> dict[str, Any]: """Make an API request. Args: method: An HTTP method. url: An API URL. access_token: An optional API access token. access_token_expiration: An optional API token expiration datetime. use_ssl: Whether to use SSL/TLS on the request. **kwargs: Additional kwargs to send with the request. Returns: An API response payload. Raises: AssertionError: To handle mypy strangeness. RequestError: Raised upon an underlying HTTP error. TokenExpiredError: Raised upon an expired access token """ if access_token_expiration and datetime.now() >= access_token_expiration: raise TokenExpiredError("Long-lived access token has expired") kwargs.setdefault("headers", {}) kwargs["headers"]["Content-Type"] = "application/json" kwargs.setdefault("params", {}) if access_token: kwargs["params"]["access_token"] = access_token if use_running_session := self._session and not self._session.closed: session = self._session else: session = ClientSession(timeout=ClientTimeout(total=DEFAULT_TIMEOUT)) try: # Only try 2x for ServerDisconnectedError to comply with the RFC: # https://datatracker.ietf.org/doc/html/rfc2616#section-8.1.4 for attempt in range(2): try: return await self._request_with_session( session, method, url, use_ssl, **kwargs ) except ServerDisconnectedError as err: # The HTTP/1.1 spec allows the device to close the connection # at any time. aiohttp raises ServerDisconnectedError to let us # decide what to do. In this case we want to retry as it likely # means the connection was stale and the server closed it on us: if attempt == 0: continue raise RequestError( f"Error requesting data from {url}: {err}" ) from err finally: if not use_running_session: await session.close() raise AssertionError # https://github.com/python/mypy/issues/8964 async def _request_with_session( self, session: ClientSession, method: str, url: URL, use_ssl: bool, **kwargs: dict[str, Any], ) -> dict[str, Any]: """Make a request with a session. Args: session: An aiohttp ClientSession. method: An HTTP method. url: An API URL. use_ssl: Whether to use SSL/TLS on the request. **kwargs: Additional kwargs to send with the request. Returns: An API response payload. Raises: RequestError: Raised upon an underlying HTTP error. """ try: async with session.request( method, url, ssl=self._ssl_context if use_ssl else None, timeout=self._request_timeout, **kwargs, ) as resp: data = await resp.json(content_type=None) except json.decoder.JSONDecodeError as err: raise RequestError("Unable to parse response as JSON") from err except ClientOSError as err: raise RequestError( f"Connection error while requesting data from {url}" ) from err except asyncio.TimeoutError as err: raise RequestError(f"Timed out while requesting data from {url}") from err LOGGER.debug("Data received for %s: %s", url, data) raise_for_error(resp, data) return cast(dict[str, Any], data) async def load_local( # pylint: disable=too-many-arguments self, host: str, password: str, port: int = DEFAULT_LOCAL_PORT, use_ssl: bool = True, skip_existing: bool = True, ) -> None: """Create a local client. Args: host: The IP address or hostname of the controller. password: The controller password. port: The port that serves the controller's API. use_ssl: Whether to use SSL/TLS on the request. skip_existing: Don't load the controller if it's already loaded. """ controller = LocalController(self._request, host, port, use_ssl) await controller.login(password) wifi_data = await controller.provisioning.wifi() if skip_existing and wifi_data["macAddress"] in self.controllers: return version_data = await controller.api.versions() controller.api_version = version_data["apiVer"] controller.hardware_version = str(version_data["hwVer"]) controller.mac = wifi_data["macAddress"] controller.software_version = version_data["swVer"] name = await controller.provisioning.device_name controller.name = str(name) self.controllers[controller.mac] = controller async def load_remote( self, email: str, password: str, skip_existing: bool = True ) -> None: """Create a remote client. Args: email: A RainMachine account email address. password: The account password. skip_existing: Don't load the controller if it's already loaded. """ auth_resp = await self._request( "post", URL("https://my.rainmachine.com/login/auth"), json={"user": {"email": email, "pwd": password, "remember": 1}}, ) access_token: str = auth_resp["access_token"] sprinklers_resp = await self._request( "post", URL("https://my.rainmachine.com/devices/get-sprinklers"), access_token=access_token, json={"user": {"email": email, "pwd": password, "remember": 1}}, ) for sprinkler in sprinklers_resp["sprinklers"]: if skip_existing and sprinkler["mac"] in self.controllers: continue controller: RemoteController = RemoteController(self._request) await controller.login(access_token, sprinkler["sprinklerId"], password) version_data = await controller.api.versions() controller.api_version = version_data["apiVer"] controller.hardware_version = str(version_data["hwVer"]) controller.mac = sprinkler["mac"] controller.name = str(sprinkler["name"]) controller.software_version = version_data["swVer"] self.controllers[sprinkler["mac"]] = controller regenmaschine-2024.03.0/regenmaschine/const.py000066400000000000000000000001311457362162600211510ustar00rootroot00000000000000"""Define package constants.""" import logging LOGGER = logging.getLogger(__package__) regenmaschine-2024.03.0/regenmaschine/controller.py000066400000000000000000000115741457362162600222230ustar00rootroot00000000000000"""Define a RainMachine controller class.""" from __future__ import annotations from collections.abc import Awaitable, Callable from datetime import datetime, timedelta from typing import Any from yarl import URL from regenmaschine.endpoints.api import API from regenmaschine.endpoints.diagnostics import Diagnostics from regenmaschine.endpoints.machine import Machine from regenmaschine.endpoints.parser import Parser from regenmaschine.endpoints.program import Program from regenmaschine.endpoints.provision import Provision from regenmaschine.endpoints.restriction import Restriction from regenmaschine.endpoints.stats import Stats from regenmaschine.endpoints.watering import Watering from regenmaschine.endpoints.zone import Zone URL_BASE_LOCAL = "{0}://{1}:{2}/api/4" URL_BASE_REMOTE = "https://api.rainmachine.com/{0}/api/4" class Controller: # pylint: disable=too-few-public-methods,too-many-instance-attributes """Define the controller.""" def __init__(self, request: Callable[..., Awaitable[dict[str, Any]]]) -> None: """Initialize. Args: request: The request method from the Client object. """ self._access_token: str | None = None self._access_token_expiration: datetime | None = None self._client_request = request self._host: str = "" self._base_url: URL = URL("") self._use_ssl = True self.api_version: str = "" self.hardware_version: str = "" self.mac: str = "" self.name: str = "" self.software_version: str = "" # API endpoints: self.api = API(self) self.diagnostics = Diagnostics(self) self.machine = Machine(self) self.parsers = Parser(self) self.programs = Program(self) self.provisioning = Provision(self) self.restrictions = Restriction(self) self.stats = Stats(self) self.watering = Watering(self) self.zones = Zone(self) async def request( self, method: str, endpoint: str, **kwargs: dict[str, Any] ) -> dict[str, Any]: """Wrap the generic request method to add access token, etc. Args: method: An HTTP method. endpoint: An API URL endpoint. **kwargs: Additional kwargs to send with the request. Returns: An API response payload. """ return await self._client_request( method, self._base_url.joinpath(endpoint), access_token=self._access_token, access_token_expiration=self._access_token_expiration, use_ssl=self._use_ssl, **kwargs, ) class LocalController(Controller): """Define a controller accessed over the LAN.""" def __init__( self, request: Callable[..., Awaitable[dict[str, Any]]], host: str, port: int, use_ssl: bool = True, ) -> None: """Initialize. Args: request: The request method from the Client object. host: The IP address or hostname of the controller. port: The port that serves the controller's API. use_ssl: Whether to use SSL/TLS on the request. """ super().__init__(request) if use_ssl: self._host = URL_BASE_LOCAL.format("https", host, port) else: self._host = URL_BASE_LOCAL.format("http", host, port) self._base_url = URL(self._host) self._use_ssl = use_ssl async def login(self, password: str) -> None: """Authenticate against the device (locally). Args: password: The controller password. """ auth_resp = await self._client_request( "post", self._base_url.joinpath("auth/login"), json={"pwd": password, "remember": 1}, ) self._access_token: str = auth_resp["access_token"] self._access_token_expiration: datetime = datetime.now() + timedelta( seconds=int(auth_resp["expires_in"]) - 10 ) class RemoteController(Controller): """Define a controller accessed over RainMachine's cloud.""" async def login( self, stage_1_access_token: str, sprinkler_id: str, password: str ) -> None: """Authenticate against the device (remotely). Args: stage_1_access_token: The first-stage access token from the remote cloud. sprinkler_id: A unique ID for the controller. password: The account password. """ auth_resp: dict = await self._client_request( "post", URL("https://my.rainmachine.com/devices/login-sprinkler"), access_token=stage_1_access_token, json={"sprinklerId": sprinkler_id, "pwd": password}, ) self._access_token = auth_resp["access_token"] self._host = URL_BASE_REMOTE.format(sprinkler_id) self._base_url = URL(self._host) regenmaschine-2024.03.0/regenmaschine/endpoints/000077500000000000000000000000001457362162600214615ustar00rootroot00000000000000regenmaschine-2024.03.0/regenmaschine/endpoints/__init__.py000066400000000000000000000027171457362162600236010ustar00rootroot00000000000000"""Define API endpoint objects.""" from __future__ import annotations from collections.abc import Awaitable, Callable from typing import TYPE_CHECKING, Generic, TypeVar from typing_extensions import ParamSpec from regenmaschine.errors import UnknownAPICallError if TYPE_CHECKING: from regenmaschine.controller import Controller _P = ParamSpec("_P") _T = TypeVar("_T", dict, list, str) class EndpointManager(Generic[_P, _T]): # pylint: disable=too-few-public-methods """Define an object to manage a API endpoints of a certain category.""" def __init__(self, controller: Controller) -> None: """Initialize. Args: controller: A Controller subclass. """ self.controller = controller @staticmethod def raise_on_gen1_controller( func: Callable[..., Awaitable[_T]], ) -> Callable[..., Awaitable[_T]]: """Raise an error if a method is called on a 1st generation controller. Args: func: The callable to decorate. Returns: The decorated callable. """ def decorator( inst: type[EndpointManager], *args: _P.args, **kwargs: _P.kwargs ) -> Awaitable[_T]: if inst.controller.hardware_version == "1": raise UnknownAPICallError( f"Can't call {func.__name__} on a 1st generation controller" ) return func(inst, *args, **kwargs) return decorator regenmaschine-2024.03.0/regenmaschine/endpoints/api.py000066400000000000000000000007621457362162600226110ustar00rootroot00000000000000"""Define an object to interact with API info.""" from __future__ import annotations from typing import Any from regenmaschine.endpoints import EndpointManager class API(EndpointManager): # pylint: disable=too-few-public-methods """Define an API object.""" async def versions(self) -> dict[str, Any]: """Get software, hardware, and API versions. Returns: An API response payload. """ return await self.controller.request("get", "apiVer") regenmaschine-2024.03.0/regenmaschine/endpoints/diagnostics.py000066400000000000000000000014221457362162600243410ustar00rootroot00000000000000"""Define an object to interact with RainMachine diagnostics.""" from __future__ import annotations from typing import Any, cast from regenmaschine.endpoints import EndpointManager class Diagnostics(EndpointManager): """Define a diagnostics object.""" @EndpointManager.raise_on_gen1_controller async def current(self) -> dict[str, Any]: """Get current diagnostics. Returns: An API response payload. """ return await self.controller.request("get", "diag") @EndpointManager.raise_on_gen1_controller async def log(self) -> str: """Get the device log. Returns: A log string. """ data = await self.controller.request("get", "diag/log") return cast(str, data["log"]) regenmaschine-2024.03.0/regenmaschine/endpoints/machine.py000066400000000000000000000033211457362162600234360ustar00rootroot00000000000000"""Define an object to interact with machine info.""" from __future__ import annotations from contextlib import suppress from typing import Any from regenmaschine.endpoints import EndpointManager from regenmaschine.errors import UnknownAPICallError class Machine(EndpointManager): """Define an API object.""" @EndpointManager.raise_on_gen1_controller async def _check_firmware_update_status(self) -> dict[str, Any]: """Check for new firmware update information. Returns: An API response payload. """ return await self.controller.request("post", "machine/update/check") async def get_firmware_update_status(self) -> dict[str, Any]: """Get the status of any pending firmware updates. Returns: An API response payload. """ with suppress(UnknownAPICallError): # 1st generation controllers don't support the POST /machine/update/check # endpoint, so if that call fails because of an UnknownAPICallError, swallow # it (with the assumption that GET /machine/update will return the needed # information): await self._check_firmware_update_status() return await self.controller.request("get", "machine/update") async def reboot(self) -> dict[str, Any]: """Reboot the controller. Returns: An API response payload. """ return await self.controller.request("post", "machine/reboot") async def update_firmware(self) -> dict[str, Any]: """Attempt to start the firmware update. Returns: An API response payload. """ return await self.controller.request("post", "machine/update") regenmaschine-2024.03.0/regenmaschine/endpoints/parser.py000066400000000000000000000020241457362162600233250ustar00rootroot00000000000000"""Define an object to interact with RainMachine weather parsers.""" from __future__ import annotations from typing import Any, cast from regenmaschine.endpoints import EndpointManager class Parser(EndpointManager): """Define a parser object.""" async def current(self) -> list[dict[str, Any]]: """Get current diagnostics. Returns: An API response payload. """ data = await self.controller.request("get", "parser") return cast(list[dict[str, Any]], data["parsers"]) async def post_data( self, payload: dict[str, list[dict[str, Any]]] ) -> dict[str, Any]: """Post weather data from an external source. Reference API Documentation for details: https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post Args: payload: A dictionary of parser data. Returns: An API response payload. """ return await self.controller.request("post", "parser/data", json=payload) regenmaschine-2024.03.0/regenmaschine/endpoints/program.py000066400000000000000000000064341457362162600235110ustar00rootroot00000000000000"""Define an object to interact with programs.""" from __future__ import annotations from typing import Any, cast from regenmaschine.endpoints import EndpointManager class Program(EndpointManager): """Define a program object.""" async def all(self, include_inactive: bool = False) -> dict[int, dict[str, Any]]: """Return all programs. Args: include_inactive: Whether to include inactive programs. Returns: An API response payload. """ data = await self.controller.request("get", "program") return { program["uid"]: program for program in data["programs"] if include_inactive or program["active"] } async def disable(self, program_id: int) -> dict[str, Any]: """Disable a program. Args: program_id: The ID of a program. Returns: An API response payload. """ return await self.controller.request( "post", f"program/{program_id}", json={"active": False} ) async def enable(self, program_id: int) -> dict[str, Any]: """Enable a program. Args: program_id: The ID of a program. Returns: An API response payload. """ return await self.controller.request( "post", f"program/{program_id}", json={"active": True} ) async def get(self, program_id: int) -> dict[str, Any]: """Return a specific program. Args: program_id: The ID of a program. Returns: An API response payload. """ return await self.controller.request("get", f"program/{program_id}") async def next(self) -> list[dict[str, Any]]: """Return the next run date/time for all programs. Returns: An API response payload. """ data = await self.controller.request("get", "program/nextrun") return cast(list[dict[str, Any]], data["nextRuns"]) async def running(self) -> list[dict[str, Any]]: """Return all running programs. Returns: An API response payload. """ data = await self.controller.request("get", "watering/program") return cast(list[dict[str, Any]], data["programs"]) async def start(self, program_id: int) -> dict[str, Any]: """Start a program. Note that in addition to including it in the query URL, the program ID must be provided in the request body to accommodate 1st generation controllers. Args: program_id: The ID of a program. Returns: An API response payload. """ return await self.controller.request( "post", f"program/{program_id}/start", json={"pid": program_id} ) async def stop(self, program_id: int) -> dict[str, Any]: """Stop a program. Note that in addition to including it in the query URL, the program ID must be provided in the request body to accommodate 1st generation controllers. Args: program_id: The ID of a program. Returns: An API response payload. """ return await self.controller.request( "post", f"program/{program_id}/stop", json={"pid": program_id} ) regenmaschine-2024.03.0/regenmaschine/endpoints/provision.py000066400000000000000000000017061457362162600240670ustar00rootroot00000000000000"""Define an object to interact with provisioning info.""" from __future__ import annotations from typing import Any, cast from regenmaschine.endpoints import EndpointManager class Provision(EndpointManager): """Define a provisioning object.""" @property async def device_name(self) -> str: """Get the name of the device. Returns: The device name. """ data = await self.controller.request("get", "provision/name") return cast(str, data["name"]) async def settings(self) -> dict[str, Any]: """Get a multitude of settings info. Returns: An API response payload. """ return await self.controller.request("get", "provision") async def wifi(self) -> dict[str, Any]: """Get wifi info from the device. Returns: An API response payload. """ return await self.controller.request("get", "provision/wifi") regenmaschine-2024.03.0/regenmaschine/endpoints/restriction.py000066400000000000000000000035541457362162600244070ustar00rootroot00000000000000"""Define an object to interact with restriction info.""" from __future__ import annotations from typing import Any, cast from regenmaschine.endpoints import EndpointManager class Restriction(EndpointManager): """Define a restriction object.""" @EndpointManager.raise_on_gen1_controller async def current(self) -> dict[str, Any]: """Get currently active restrictions. Returns: An API response payload. """ return await self.controller.request("get", "restrictions/currently") @EndpointManager.raise_on_gen1_controller async def hourly(self) -> list[dict[str, Any]]: """Get a list of restrictions that are active over the next hour. Returns: An API response payload. """ data = await self.controller.request("get", "restrictions/hourly") return cast(list[dict[str, Any]], data["hourlyRestrictions"]) async def raindelay(self) -> dict[str, Any]: """Get restriction info related to rain delays. Returns: An API response payload. """ return await self.controller.request("get", "restrictions/raindelay") @EndpointManager.raise_on_gen1_controller async def set_universal(self, payload: dict[str, Any]) -> dict[str, Any]: """Set global (always active) restrictions based on a payload. Args: payload: An API request payload. Returns: An API response payload. """ return await self.controller.request( "post", "restrictions/global", json=payload ) @EndpointManager.raise_on_gen1_controller async def universal(self) -> dict[str, Any]: """Get global (always active) restrictions. Returns: An API response payload. """ return await self.controller.request("get", "restrictions/global") regenmaschine-2024.03.0/regenmaschine/endpoints/stats.py000066400000000000000000000022421457362162600231710ustar00rootroot00000000000000"""Define an object to interact with RainMachine statistics.""" from __future__ import annotations import datetime from typing import Any, cast from regenmaschine.endpoints import EndpointManager class Stats(EndpointManager): """Define a statistics object.""" async def on_date(self, date: datetime.date) -> dict[str, Any]: """Get statistics for a certain date. Args: date: The date to examine. Returns: An API response payload. """ return await self.controller.request( "get", f"dailystats/{date.strftime('%Y-%m-%d')}" ) async def upcoming(self, details: bool = False) -> list[dict[str, Any]]: """Return watering statistics for the next 6 days. Args: details: Whether extra details should be included. Returns: An API response payload. """ endpoint = "dailystats" data_key = "DailyStats" if details: endpoint += "/details" data_key = "DailyStatsDetails" data = await self.controller.request("get", endpoint) return cast(list[dict[str, Any]], data[data_key]) regenmaschine-2024.03.0/regenmaschine/endpoints/watering.py000066400000000000000000000102601457362162600236520ustar00rootroot00000000000000"""Define an object to interact with generic watering data/actions.""" from __future__ import annotations import datetime from typing import Any, cast from regenmaschine.endpoints import EndpointManager MAX_PAUSE_DURATION = 43200 class Watering(EndpointManager): """Define a watering object.""" @EndpointManager.raise_on_gen1_controller async def log( self, date: datetime.date | None = None, days: int | None = None, details: bool = False, ) -> list[dict[str, Any]]: """Get watering information for X days from Y date. Args: date: The date to examine. days: The number of days' worth of logs to retrieve. details: Whether to include extra details. Returns: An API response payload. """ endpoint = "watering/log" if details: endpoint += "/details" if date and days: endpoint = f"{endpoint}/{date.strftime('%Y-%m-%d')}/{days}" data = await self.controller.request("get", endpoint) return cast(list[dict[str, Any]], data["waterLog"]["days"]) @EndpointManager.raise_on_gen1_controller async def flowmeter(self) -> dict[str, Any]: """Return the registered values from flowmeter. Returns: An API response payload. """ return await self.controller.request("get", "watering/flowmeter") @EndpointManager.raise_on_gen1_controller async def post_flowmeter( self, value: float, units: str = "litre" ) -> dict[str, Any]: """Add values to flowmeter counters from an external meter. Args: value: The flow meter value. units: A valid unit ("clicks", "gal", "m3" and "litre"). Returns: An API response payload. """ return await self.controller.request( "post", "watering/flowmeter", json={"value": value, "units": units} ) @EndpointManager.raise_on_gen1_controller async def pause_all(self, seconds: int) -> dict[str, Any]: """Pause all watering for a specified number of seconds. Args: seconds: The number of seconds to pause. Returns: An API response payload. Raises: ValueError: Raised if the maximum duration is exceeded. """ if seconds > MAX_PAUSE_DURATION: raise ValueError( f"Cannot pause watering for more than {MAX_PAUSE_DURATION} seconds" ) return await self.controller.request( "post", "watering/pauseall", json={"duration": seconds} ) @EndpointManager.raise_on_gen1_controller async def queue(self) -> list[dict[str, Any]]: """Return the queue of active watering activities. Returns: An API response payload. """ data = await self.controller.request("get", "watering/queue") return cast(list[dict[str, Any]], data["queue"]) @EndpointManager.raise_on_gen1_controller async def runs( self, date: datetime.date | None = None, days: int | None = None ) -> list[dict[str, Any]]: """Return all program runs for X days from Y date. Args: date: The date to examine. days: The number of days' worth of runs to retrieve. Returns: An API response payload. """ endpoint = "watering/past" if date and days: endpoint = f"{endpoint}/{date.strftime('%Y-%m-%d')}/{days}" data = await self.controller.request("get", endpoint) return cast(list[dict[str, Any]], data["pastValues"]) @EndpointManager.raise_on_gen1_controller async def stop_all(self) -> dict[str, Any]: """Stop all programs and zones from running. Returns: An API response payload. """ return await self.controller.request("post", "watering/stopall") @EndpointManager.raise_on_gen1_controller async def unpause_all(self) -> dict[str, Any]: """Unpause all paused watering. Returns: An API response payload. """ data = await self.pause_all(0) return cast(dict[str, Any], data) regenmaschine-2024.03.0/regenmaschine/endpoints/zone.py000066400000000000000000000102761457362162600230140ustar00rootroot00000000000000"""Define an object to interact with zones.""" from __future__ import annotations from typing import TYPE_CHECKING, Any, cast from regenmaschine.endpoints import EndpointManager class Zone(EndpointManager): """Define a zone object.""" async def _post(self, zone_id: int, json: dict[str, Any]) -> dict[str, Any]: """Post data to a (non)existing zone. Args: zone_id: A zone ID. json: An API request payload. Returns: An API response payload. """ return await self.controller.request( "post", f"zone/{zone_id}/properties", json=json ) async def all( self, *, details: bool = False, include_inactive: bool = False ) -> dict[int, dict[str, Any]]: """Return all zones (with optional advanced properties). Args: details: Whether extra details should be included. include_inactive: Whether to include inactive programs. Returns: An API response payload. """ raw_zones = await self.controller.request("get", "zone") zone_details: dict | None = None if details: zone_details = await self.controller.request("get", "zone/properties") zones: dict[int, dict[str, Any]] = {} for zone in raw_zones["zones"]: if details: if TYPE_CHECKING: assert zone_details is not None [extra] = [z for z in zone_details["zones"] if z["uid"] == zone["uid"]] zone_data = {**zone, **extra} else: if "active" not in zone: zone["active"] = True zone_data = zone if zone_data["active"] or include_inactive: zones[zone_data["uid"]] = zone_data return zones async def disable(self, zone_id: int) -> dict[str, Any]: """Disable a zone. Args: zone_id: A zone ID. Returns: An API response payload. """ return await self._post(zone_id, {"active": False}) async def enable(self, zone_id: int) -> dict[str, Any]: """Enable a zone. Args: zone_id: A zone ID. Returns: An API response payload. """ return await self._post(zone_id, {"active": True}) async def get(self, zone_id: int, *, details: bool = False) -> dict[str, Any]: """Return a specific zone. Args: zone_id: A zone ID. details: Whether extra details should be included. Returns: An API response payload. """ zone = await self.controller.request("get", f"zone/{zone_id}") if not details: return zone zone_details = await self.controller.request( "get", f"zone/{zone_id}/properties" ) return {**zone, **zone_details} async def running(self) -> list[dict[str, Any]]: """Return all running zones. Returns: An API response payload. """ data = await self.controller.request("get", "watering/zone") return cast(list[dict[str, Any]], data["zones"]) async def start(self, zone_id: int, time: int) -> dict[str, Any]: """Start a zone. Note that in addition to including it in the query URL, the zone ID must be provided in the request body to accommodate 1st generation controllers. Args: zone_id: A zone ID. time: The number of seconds to run the zone for. Returns: An API response payload. """ return await self.controller.request( "post", f"zone/{zone_id}/start", json={"time": time, "zid": zone_id} ) async def stop(self, zone_id: int) -> dict[str, Any]: """Stop a zone. Note that in addition to including it in the query URL, the zone ID must be provided in the request body to accommodate 1st generation controllers. Args: zone_id: A zone ID. Returns: An API response payload. """ return await self.controller.request( "post", f"zone/{zone_id}/stop", json={"zid": zone_id} ) regenmaschine-2024.03.0/regenmaschine/errors.py000066400000000000000000000061421457362162600213470ustar00rootroot00000000000000"""Define package errors.""" from __future__ import annotations from typing import Any from aiohttp import ClientResponse from aiohttp.client_exceptions import ClientError from yarl import URL class RainMachineError(Exception): """Define a base error.""" pass class RequestError(RainMachineError): """Define an error related to invalid requests.""" pass class TokenExpiredError(RequestError): """Define an error for expired access tokens that can't be refreshed.""" pass class UnknownAPICallError(RequestError): """Define an error for an unknown API call.""" pass class UnvalidatedEmailError(RequestError): """Define an error for unvalidated email addresses.""" pass LOCAL_ERROR_CODE_EXCEPTION_MAPPING = { 2: TokenExpiredError, 13: UnknownAPICallError, } REMOTE_ERROR_CODE_EXCEPTION_MAPPING = { 1: UnvalidatedEmailError("The email has not been validated"), } def _raise_local_api_error(url: URL, error_code: int, message: str) -> None: """Raise the appropriate error for a remote error code. Args: url: The URL that raised the exception: error_code: The RainMachine error code. message: The RainMachine error message. Raises: exc: A RequestError subclass. """ exc: RequestError try: exc_obj = LOCAL_ERROR_CODE_EXCEPTION_MAPPING[error_code] exc = exc_obj(message) except KeyError: exc = RequestError( f"Unknown error returned for {url}: {error_code} -> {message}" ) raise exc def _raise_remote_api_error(url: URL, error_code: int) -> None: """Raise the appropriate error for a remote error code. Args: url: The URL that raised the exception: error_code: The RainMachine error code. Raises: exc: A RequestError subclass. """ exc: RequestError try: exc = REMOTE_ERROR_CODE_EXCEPTION_MAPPING[error_code] except KeyError: exc = RequestError(f"Unknown error code returned for {url}: {error_code}") raise exc def raise_for_error(resp: ClientResponse, data: dict[str, Any] | None) -> None: """Raise an error from the remote API if necessary. Args: resp: The aiohttp ClientResponse that generated the exception. data: An optional API response payload. Raises: RequestError: Raised when any error occurs. """ if data: if data.get("errorType") and data["errorType"] > 0: # RainMachine's remote cloud uses "errorType" to show errors, so if we find # that, we assume we need to raise a remote error: _raise_remote_api_error(resp.url, data["errorType"]) if data.get("statusCode") and data["statusCode"] != 200: # RainMachine's local cloud uses "statusCode" to show errors, so if we find # that, we assume we need to raise a local error: _raise_local_api_error(resp.url, data["statusCode"], data["message"]) else: try: resp.raise_for_status() except ClientError as err: raise RequestError(f"Error while requesting {resp.url}: {err}") from err regenmaschine-2024.03.0/regenmaschine/py.typed000066400000000000000000000000001457362162600211430ustar00rootroot00000000000000regenmaschine-2024.03.0/script/000077500000000000000000000000001457362162600161525ustar00rootroot00000000000000regenmaschine-2024.03.0/script/release000077500000000000000000000024631457362162600175250ustar00rootroot00000000000000#!/usr/bin/env bash set -e REPO_PATH="$( dirname "$( cd "$(dirname "$0")" ; pwd -P )" )" if [ "$(git rev-parse --abbrev-ref HEAD)" != "dev" ]; then echo "Refusing to publish a release from a branch other than dev" exit 1 fi if [ -z "$(command -v poetry)" ]; then echo "Poetry needs to be installed to run this script: pip3 install poetry" exit 1 fi function generate_version { latest_tag="$(git tag --sort=committerdate | tail -1)" month="$(date +'%Y.%m')" if [[ "$latest_tag" =~ "$month".* ]]; then patch="$(echo "$latest_tag" | cut -d . -f 3)" ((patch=patch+1)) echo "$month.$patch" else echo "$month.0" fi } # Temporarily uninstall pre-commit hooks so that we can push to dev and main: pre-commit uninstall # Pull the latest dev: git pull origin dev # Generate the next version (in the format YEAR.MONTH.RELEASE_NUMER): new_version=$(generate_version) # Update the PyPI package version: sed -i "" "s/^version = \".*\"/version = \"$new_version\"/g" "$REPO_PATH/pyproject.toml" git add pyproject.toml # Commit, tag, and push: git commit -m "Bump version to $new_version" git tag "$new_version" git push && git push --tags # Merge dev into main: git checkout main git merge dev git push git checkout dev # Re-initialize pre-commit: pre-commit install regenmaschine-2024.03.0/script/setup000077500000000000000000000002571457362162600172440ustar00rootroot00000000000000#!/bin/sh set -e if command -v "mise"; then mise install fi # Install all dependencies: pip3 install poetry poetry install # Install pre-commit hooks: pre-commit install regenmaschine-2024.03.0/tests/000077500000000000000000000000001457362162600160105ustar00rootroot00000000000000regenmaschine-2024.03.0/tests/__init__.py000066400000000000000000000000341457362162600201160ustar00rootroot00000000000000"""Define package tests.""" regenmaschine-2024.03.0/tests/common.py000066400000000000000000000014561457362162600176600ustar00rootroot00000000000000"""Define common test utilities.""" import os TEST_ACCESS_TOKEN = "12345abcdef" # noqa: S105 TEST_API_VERSION = "4.5.0" TEST_EMAIL = "user@host.com" TEST_HOST = "192.168.1.100" TEST_HW_VERSION = "3" TEST_MAC = "ab:cd:ef:12:34:56" TEST_NAME = "My House" TEST_PASSWORD = "the_password_123" # noqa: S105 TEST_PORT = 8081 TEST_SPRINKLER_ID = "12345abcde" TEST_SW_VERSION = "4.0.925" TEST_TOTP_CODE = 123456 TEST_URL = f"https://{TEST_HOST}:{TEST_PORT}" def load_fixture(filename: str) -> str: """Load a fixture. Args: filename: The filename of the fixtures/ file to load. Returns: A string containing the contents of the file. """ path = os.path.join(os.path.dirname(__file__), "fixtures", filename) with open(path, encoding="utf-8") as fptr: return fptr.read() regenmaschine-2024.03.0/tests/conftest.py000066400000000000000000000121631457362162600202120ustar00rootroot00000000000000"""Define fixtures available for all tests.""" import json from typing import Any, cast import aiohttp import pytest from aresponses import ResponsesMockServer from .common import TEST_HOST, TEST_PORT, TEST_SPRINKLER_ID, load_fixture @pytest.fixture(name="api_version_response") def api_version_response_fixture() -> dict[str, Any]: """Return an API response that contains versions. Returns: An API response payload. """ return cast(dict[str, Any], json.loads(load_fixture("api_version_response.json"))) @pytest.fixture(name="auth_login_response") def auth_login_response_fixture() -> dict[str, Any]: """Return an API response that contains authentication info. Returns: An API response payload. """ return cast(dict[str, Any], json.loads(load_fixture("auth_login_response.json"))) @pytest.fixture(name="authenticated_local_client") def authenticated_local_client_fixture( api_version_response: dict[str, Any], auth_login_response: dict[str, Any], provision_name_response: dict[str, Any], provision_wifi_response: dict[str, Any], ) -> ResponsesMockServer: """Return an aresponses server for an authenticated local client. Args: api_version_response: An API response payload. auth_login_response: An API response payload. provision_name_response: An API response payload. provision_wifi_response: An API response payload. Returns: A mocked local API connect. """ client = ResponsesMockServer() client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/auth/login", "post", response=aiohttp.web_response.json_response(auth_login_response, status=200), ) client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/provision/name", "get", response=aiohttp.web_response.json_response( provision_name_response, status=200 ), ) client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/provision/wifi", "get", response=aiohttp.web_response.json_response( provision_wifi_response, status=200 ), ) client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/apiVer", "get", response=aiohttp.web_response.json_response(api_version_response, status=200), ) return client @pytest.fixture(name="authenticated_remote_client") def authenticated_remote_client_fixture( api_version_response: dict[str, Any], remote_auth_login_1_response: dict[str, Any], remote_sprinklers_response: dict[str, Any], ) -> ResponsesMockServer: """Return an aresponses server for an authenticated remote client. Args: api_version_response: An API response payload. remote_auth_login_1_response: An API response payload. remote_sprinklers_response: An API response payload. Returns: A mocked remote API connect. """ client = ResponsesMockServer() client.add( "my.rainmachine.com", "/login/auth", "post", response=aiohttp.web_response.json_response( remote_auth_login_1_response, status=200 ), ) client.add( "my.rainmachine.com", "/devices/get-sprinklers", "post", response=aiohttp.web_response.json_response( remote_sprinklers_response, status=200 ), ) client.add( "my.rainmachine.com", "/devices/login-sprinkler", "post", response=aiohttp.web_response.json_response( json.loads(load_fixture("remote_auth_login_2_response.json")), status=200 ), ) client.add( "api.rainmachine.com", f"/{TEST_SPRINKLER_ID}/api/4/apiVer", "get", response=aiohttp.web_response.json_response(api_version_response, status=200), ) return client @pytest.fixture(name="provision_name_response") def provision_name_response_fixture() -> dict[str, Any]: """Return an API response that contains the controller name. Returns: An API response payload. """ return cast( dict[str, Any], json.loads(load_fixture("provision_name_response.json")) ) @pytest.fixture(name="provision_wifi_response") def provision_wifi_response_fixture() -> dict[str, Any]: """Return an API response that contains WiFi info. Returns: An API response payload. """ return cast( dict[str, Any], json.loads(load_fixture("provision_wifi_response.json")) ) @pytest.fixture(name="remote_auth_login_1_response") def remote_auth_login_1_response_fixture() -> dict[str, Any]: """Return an API response that contains first-stage remote login info. Returns: An API response payload. """ return cast( dict[str, Any], json.loads(load_fixture("remote_auth_login_1_response.json")) ) @pytest.fixture(name="remote_sprinklers_response") def remote_sprinklers_response_fixture() -> dict[str, Any]: """Return an API response that contains first-stage remote sprinkler info. Returns: An API response payload. """ return cast( dict[str, Any], json.loads(load_fixture("remote_sprinklers_response.json")) ) regenmaschine-2024.03.0/tests/endpoints/000077500000000000000000000000001457362162600200135ustar00rootroot00000000000000regenmaschine-2024.03.0/tests/endpoints/__init__.py000066400000000000000000000000421457362162600221200ustar00rootroot00000000000000"""Define tests for endpoints.""" regenmaschine-2024.03.0/tests/endpoints/test_api.py000066400000000000000000000050531457362162600222000ustar00rootroot00000000000000"""Define tests for api endpoints.""" from typing import Any import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from tests.common import TEST_HOST, TEST_PASSWORD, TEST_PORT @pytest.mark.asyncio async def test_api_versions( aresponses: ResponsesMockServer, api_version_response: dict[str, Any], authenticated_local_client: ResponsesMockServer, ) -> None: """Test getting API, hardware, and software versions. Args: aresponses: An aresponses server. api_version_response: An API response payload. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/apiVer", "get", response=aiohttp.web_response.json_response( api_version_response, status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.api.versions() assert data["apiVer"] == "4.5.0" assert data["hwVer"] == 3 assert data["swVer"] == "4.0.925" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_api_versions_no_explicit_session( aresponses: ResponsesMockServer, api_version_response: dict[str, Any], authenticated_local_client: ResponsesMockServer, ) -> None: """Test no explicit ClientSession. Args: aresponses: An aresponses server. api_version_response: An API response payload. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/apiVer", "get", response=aiohttp.web_response.json_response( api_version_response, status=200 ), ) client = Client() await client.load_local(TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False) controller = next(iter(client.controllers.values())) data = await controller.api.versions() assert data["apiVer"] == "4.5.0" assert data["hwVer"] == 3 assert data["swVer"] == "4.0.925" aresponses.assert_plan_strictly_followed() regenmaschine-2024.03.0/tests/endpoints/test_diagnostics.py000066400000000000000000000045551457362162600237440ustar00rootroot00000000000000"""Define tests for diagnostics endpoints.""" import json import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from tests.common import TEST_HOST, TEST_PASSWORD, TEST_PORT, load_fixture @pytest.mark.asyncio async def test_diagnostics_current( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test retrieving current diagnostics. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/diag", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("diag_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.diagnostics.current() assert data["memUsage"] == 18220 aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_diagnostics_log( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test retrieving the entire diagnostics log. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/diag/log", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("diag_log_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.diagnostics.log() assert data == "----" aresponses.assert_plan_strictly_followed() regenmaschine-2024.03.0/tests/endpoints/test_machine.py000066400000000000000000000150661457362162600230400ustar00rootroot00000000000000"""Define tests for api endpoints.""" import json from typing import Any, cast import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from tests.common import TEST_HOST, TEST_PASSWORD, TEST_PORT, load_fixture @pytest.fixture(name="machine_check_update_response") def machine_check_update_response_fixture() -> dict[str, Any]: """Return an API response that contains firmware update check data. Returns: An API response payload. """ return cast( dict[str, Any], json.loads(load_fixture("machine_check_update_response.json")) ) @pytest.fixture(name="machine_get_update_response") def machine_get_update_response_fixture() -> dict[str, Any]: """Return an API response that contains firmware update data. Returns: An API response payload. """ return cast( dict[str, Any], json.loads(load_fixture("machine_get_update_response.json")) ) @pytest.mark.asyncio async def test_get_firmware_update_status( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, machine_check_update_response: dict[str, Any], machine_get_update_response: dict[str, Any], ) -> None: """Test getting the status of a firmware update. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. machine_check_update_response: An API response payload. machine_get_update_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/machine/update/check", "post", response=aiohttp.web_response.json_response( machine_check_update_response, status=200 ), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/machine/update", "get", response=aiohttp.web_response.json_response( machine_get_update_response, status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.machine.get_firmware_update_status() assert data["update"] is False assert data["updateStatus"] == 1 assert len(data["packageDetails"]) == 2 aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_get_firmware_update_status_gen1( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, machine_get_update_response: dict[str, Any], ) -> None: """Test getting the status of a firmware update on a 1st generation controller. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. machine_get_update_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/machine/update", "get", response=aiohttp.web_response.json_response( machine_get_update_response, status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) # Simulate this controller being a 1st generation controller: controller.hardware_version = "1" data = await controller.machine.get_firmware_update_status() assert data["update"] is False assert data["updateStatus"] == 1 aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_reboot( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test requesting a reboot. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/machine/reboot", "post", response=aiohttp.web_response.json_response( json.loads(load_fixture("machine_reboot_response.json")), status=200, ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.machine.reboot() assert data["message"] == "OK" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_update_firmware( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, machine_check_update_response: dict[str, Any], ) -> None: """Test requesting a firmware update. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. machine_check_update_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/machine/update/check", "post", response=aiohttp.web_response.json_response( machine_check_update_response, status=200 ), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/machine/update", "post", response=aiohttp.web_response.json_response( json.loads(load_fixture("machine_update_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.machine.update_firmware() assert data["message"] == "OK" aresponses.assert_plan_strictly_followed() regenmaschine-2024.03.0/tests/endpoints/test_parser.py000066400000000000000000000050261457362162600227230ustar00rootroot00000000000000"""Define tests for parser endpoints.""" import json import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from tests.common import TEST_HOST, TEST_PASSWORD, TEST_PORT, load_fixture @pytest.mark.asyncio async def test_parsers_current( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, ) -> None: """Test getting all current parsers. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/parser", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("parser_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.parsers.current() assert len(data) == 1 assert data[0]["name"] == "NOAA Parser" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_parsers_post_data( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, ) -> None: """Test pushing data to parser. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/parser/data", "post", response=aiohttp.web_response.json_response( json.loads(load_fixture("parser_post_data_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) payload = json.loads(load_fixture("parser_post_data_payload.json")) data = await controller.parsers.post_data(payload) assert len(data) == 2 assert data["message"] == "OK" aresponses.assert_plan_strictly_followed() regenmaschine-2024.03.0/tests/endpoints/test_program.py000066400000000000000000000233441457362162600231010ustar00rootroot00000000000000"""Define tests for program endpoints.""" import json from typing import Any, cast import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from tests.common import TEST_HOST, TEST_PASSWORD, TEST_PORT, load_fixture @pytest.fixture(name="program_post_response") def program_post_response_fixture() -> dict[str, Any]: """Return an API response that contains program post data. Returns: An API response payload. """ return cast(dict[str, Any], json.loads(load_fixture("program_post_response.json"))) @pytest.fixture(name="program_response") def program_response_fixture() -> dict[str, Any]: """Return an API response that contains program data. Returns: An API response payload. """ return cast(dict[str, Any], json.loads(load_fixture("program_response.json"))) @pytest.fixture(name="program_start_stop_response") def program_start_stop_response_fixture() -> dict[str, Any]: """Return an API response that contains program start/stop data. Returns: An API response payload. """ return cast( dict[str, Any], json.loads(load_fixture("program_start_stop_response.json")) ) @pytest.mark.asyncio async def test_program_enable_disable( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, program_post_response: dict[str, Any], ) -> None: """Test enabling a program. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. program_post_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/program/1", "post", response=aiohttp.web_response.json_response( program_post_response, status=200 ), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/program/1", "post", response=aiohttp.web_response.json_response( program_post_response, status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) resp = await controller.programs.enable(1) assert resp["message"] == "OK" resp = await controller.programs.disable(1) assert resp["message"] == "OK" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_program_get( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, program_response: dict[str, Any], ) -> None: """Test getting all programs. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. program_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/program", "get", response=aiohttp.web_response.json_response(program_response, status=200), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) programs = await controller.programs.all(include_inactive=True) assert len(programs) == 2 assert programs[1]["name"] == "Morning" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_program_get_active( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, program_response: dict[str, Any], ) -> None: """Test getting only active programs. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. program_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/program", "get", response=aiohttp.web_response.json_response(program_response, status=200), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) programs = await controller.programs.all() assert len(programs) == 1 assert programs[1]["name"] == "Morning" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_program_get_by_id( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting a program by its ID. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/program/1", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("program_id_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.programs.get(1) assert data["name"] == "Morning" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_program_next_run( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting the next run of a program. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/program/nextrun", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("program_nextrun_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.programs.next() assert len(data) == 2 aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_program_running( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting all running programs. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/watering/program", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("watering_program_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.programs.running() assert len(data) == 1 assert data[0]["name"] == "Evening" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_program_start_and_stop( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, program_start_stop_response: dict[str, Any], ) -> None: """Test starting and stopping a program. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. program_start_stop_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/program/1/start", "post", response=aiohttp.web_response.json_response( program_start_stop_response, status=200 ), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/program/1/stop", "post", response=aiohttp.web_response.json_response( program_start_stop_response, status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.programs.start(1) assert data["message"] == "OK" data = await controller.programs.stop(1) assert data["message"] == "OK" aresponses.assert_plan_strictly_followed() regenmaschine-2024.03.0/tests/endpoints/test_provision.py000066400000000000000000000044011457362162600234530ustar00rootroot00000000000000"""Define tests for program endpoints.""" import json from typing import Any import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from tests.common import TEST_HOST, TEST_PASSWORD, TEST_PORT, load_fixture @pytest.mark.asyncio async def test_endpoints( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, provision_name_response: dict[str, Any], provision_wifi_response: dict[str, Any], ) -> None: """Test getting all provisioning data. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. provision_name_response: An API response payload. provision_wifi_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/provision", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("provision_response.json")), status=200 ), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/provision/name", "get", response=aiohttp.web_response.json_response( provision_name_response, status=200 ), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/provision/wifi", "get", response=aiohttp.web_response.json_response( provision_wifi_response, status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) name = await controller.provisioning.device_name assert name == "My House" data = await controller.provisioning.settings() assert data["system"]["databasePath"] == "/rainmachine-app/DB/Default" assert data["location"]["stationName"] == "MY STATION" aresponses.assert_plan_strictly_followed() regenmaschine-2024.03.0/tests/endpoints/test_restrictions.py000066400000000000000000000155501457362162600241620ustar00rootroot00000000000000"""Define tests for restriction endpoints.""" import json import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from regenmaschine.errors import UnknownAPICallError from tests.common import TEST_HOST, TEST_PASSWORD, TEST_PORT, load_fixture @pytest.mark.asyncio async def test_restrictions_hourly( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting any hourly restrictions. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/restrictions/hourly", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("restrictions_hourly_response.json")), status=200, ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.restrictions.hourly() assert not data aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_restrictions_hourly_gen1( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test that getting hourly restrictions fails on a Gen1 controller. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) # Simulate this controller being a 1st generation controller: controller.hardware_version = "1" with pytest.raises(UnknownAPICallError): _ = await controller.restrictions.hourly() aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_restrictions_current( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting any current restrictions. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/restrictions/currently", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("restrictions_currently_response.json")), status=200, ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.restrictions.current() assert data["hourly"] is False aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_restrictions_global( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting any global restrictions. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/restrictions/global", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("restrictions_global_response.json")), status=200, ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.restrictions.universal() assert data["freezeProtectTemp"] == 2 aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_restrictions_raindelay( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting any rain delay-related restrictions. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/restrictions/raindelay", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("restrictions_raindelay_response.json")), status=200, ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.restrictions.raindelay() assert data["delayCounter"] == -1 aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_set_restrictions_global( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test setting global restrictions. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/restrictions/global", "post", response=aiohttp.web_response.json_response( json.loads(load_fixture("restrictions_global_set_response.json")), status=200, ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) await controller.restrictions.set_universal( {"hotDaysExtraWatering": False, "freezeProtectEnabled": True} ) aresponses.assert_plan_strictly_followed() regenmaschine-2024.03.0/tests/endpoints/test_stats.py000066400000000000000000000035661457362162600225740ustar00rootroot00000000000000"""Define tests for stat endpoints.""" import json from datetime import date import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from tests.common import TEST_HOST, TEST_PASSWORD, TEST_PORT, load_fixture @pytest.mark.asyncio async def test_stats( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting states (with or without details). Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: today = date.today() today_str = today.strftime("%Y-%m-%d") authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", f"/api/4/dailystats/{today_str}", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("dailystats_date_response.json")), status=200 ), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/dailystats/details", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("dailystats_details_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) on_date_data = await controller.stats.on_date(today) assert on_date_data["percentage"] == 100 upcoming_data = await controller.stats.upcoming(details=True) assert len(upcoming_data[0]["programs"]) == 4 aresponses.assert_plan_strictly_followed() regenmaschine-2024.03.0/tests/endpoints/test_watering.py000066400000000000000000000217251457362162600232530ustar00rootroot00000000000000"""Define tests for restriction endpoints.""" import datetime import json from typing import Any, cast import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from tests.common import TEST_HOST, TEST_PASSWORD, TEST_PORT, load_fixture @pytest.fixture(name="watering_pause_response") def watering_pause_response_fixture() -> dict[str, Any]: """Return an API response that contains watering pause data. Returns: An API response payload. """ return cast( dict[str, Any], json.loads(load_fixture("watering_pause_response.json")) ) @pytest.mark.asyncio async def test_watering_log_details( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting watering log details. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: today = datetime.date.today() today_str = today.strftime("%Y-%m-%d") authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", f"/api/4/watering/log/details/{today_str}/2", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("watering_log_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.watering.log(today, 2, details=True) assert len(data) == 2 aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_watering_pause( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, watering_pause_response: dict[str, Any], ) -> None: """Test pausing and unpausing watering. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. watering_pause_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/watering/pauseall", "post", response=aiohttp.web_response.json_response( watering_pause_response, status=200 ), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/watering/pauseall", "post", response=aiohttp.web_response.json_response( watering_pause_response, status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.watering.pause_all(30) assert data["message"] == "OK" with pytest.raises(ValueError): data = await controller.watering.pause_all(60 * 60 * 24) data = await controller.watering.unpause_all() assert data["message"] == "OK" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_watering_queue( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting the watering queue. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/watering/queue", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("watering_queue_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.watering.queue() assert not data aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_watering_past( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test gettinng info on past watering runs. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: today = datetime.date.today() today_str = today.strftime("%Y-%m-%d") authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", f"/api/4/watering/past/{today_str}/2", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("watering_past_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.watering.runs(today, 2) assert len(data) == 8 aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_watering_stop( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test stopping all watering activities. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/watering/stopall", "post", response=aiohttp.web_response.json_response( json.loads(load_fixture("watering_stopall_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.watering.stop_all() assert data["message"] == "OK" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_watering_flowmeter( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting flowmeter values. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/watering/flowmeter", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("watering_flowmeter_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.watering.flowmeter() assert data["flowMeterWateringClicks"] == 4000 aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_post_watering_flowmeter( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting flowmeter values. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/watering/flowmeter", "post", response=aiohttp.web_response.json_response( json.loads(load_fixture("watering_flowmeter_post_response.json")), status=200, ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.watering.post_flowmeter(value=200, units="litre") assert data["statusCode"] == 0 aresponses.assert_plan_strictly_followed() regenmaschine-2024.03.0/tests/endpoints/test_zone.py000066400000000000000000000253771457362162600224150ustar00rootroot00000000000000"""Define tests for program endpoints.""" import json from typing import Any, cast import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from tests.common import TEST_HOST, TEST_PASSWORD, TEST_PORT, load_fixture @pytest.fixture(name="zone_id_response") def zone_id_response_fixture() -> dict[str, Any]: """Return an API response that contains zone ID data. Returns: An API response payload. """ return cast(dict[str, Any], json.loads(load_fixture("zone_id_response.json"))) @pytest.fixture(name="zone_post_response") def zone_post_response_fixture() -> dict[str, Any]: """Return an API response that contains zone post data. Returns: An API response payload. """ return cast(dict[str, Any], json.loads(load_fixture("zone_post_response.json"))) @pytest.fixture(name="zone_start_stop_response") def zone_start_stop_response_fixture() -> dict[str, Any]: """Return an API response that contains zone start/stop data. Returns: An API response payload. """ return cast( dict[str, Any], json.loads(load_fixture("zone_start_stop_response.json")) ) @pytest.mark.asyncio async def test_zone_enable_disable( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, zone_post_response: dict[str, Any], ) -> None: """Test enabling a zone. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. zone_post_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone/1/properties", "post", response=aiohttp.web_response.json_response(zone_post_response, status=200), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone/1/properties", "post", response=aiohttp.web_response.json_response(zone_post_response, status=200), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) resp = await controller.zones.enable(1) assert resp["message"] == "OK" resp = await controller.zones.disable(1) assert resp["message"] == "OK" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio @pytest.mark.parametrize( "zone_fixture_filename", ["zone_response.json", "zone_response_gen1.json"] ) async def test_zone_get( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, zone_fixture_filename: str, ) -> None: """Test getting info on all zones. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. zone_fixture_filename: A fixture filename. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture(zone_fixture_filename)), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) zones = await controller.zones.all(include_inactive=True) assert len(zones) == 12 assert zones[1]["name"] == "Landscaping" assert zones[1]["active"] is True aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio @pytest.mark.parametrize( "zone_fixture_filename", ["zone_response.json", "zone_response_gen1.json"] ) async def test_zone_get_detail( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, zone_fixture_filename: str, ) -> None: """Test getting all zones with details. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. zone_fixture_filename: A fixture filename. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture(zone_fixture_filename)), status=200 ), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone/properties", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("zone_properties_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) zones = await controller.zones.all(details=True) assert len(zones) == 2 assert zones[1]["name"] == "Landscaping" assert zones[1]["active"] is True assert zones[1]["ETcoef"] == 0.80000000000000004 aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_zone_get_by_id( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, zone_id_response: dict[str, Any], ) -> None: """Test getting properties on a specific zone by ID. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. zone_id_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone/1", "get", response=aiohttp.web_response.json_response(zone_id_response, status=200), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.zones.get(1) assert data["name"] == "Landscaping" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_zone_get_by_id_details( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, zone_id_response: dict[str, Any], ) -> None: """Test getting advanced properties on a specific zone by ID. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. zone_id_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone/1", "get", response=aiohttp.web_response.json_response(zone_id_response, status=200), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone/1/properties", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("zone_id_properties_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.zones.get(1, details=True) assert data["name"] == "Landscaping" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_zone_running( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test getting all running zones. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/watering/zone", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("watering_zone_response.json")), status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.zones.running() assert len(data) == 12 assert data[0]["name"] == "Zone 1" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_zone_start_stop( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer, zone_start_stop_response: dict[str, Any], ) -> None: """Test starting and stopping a zone. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. zone_start_stop_response: An API response payload. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone/1/start", "post", response=aiohttp.web_response.json_response( zone_start_stop_response, status=200 ), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone/1/stop", "post", response=aiohttp.web_response.json_response( zone_start_stop_response, status=200 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) data = await controller.zones.start(1, 60) assert data["message"] == "OK" data = await controller.zones.stop(1) assert data["message"] == "OK" aresponses.assert_plan_strictly_followed() regenmaschine-2024.03.0/tests/fixtures/000077500000000000000000000000001457362162600176615ustar00rootroot00000000000000regenmaschine-2024.03.0/tests/fixtures/__init__.py000066400000000000000000000000271457362162600217710ustar00rootroot00000000000000"""Define fixtures.""" regenmaschine-2024.03.0/tests/fixtures/api_version_response.json000066400000000000000000000000741457362162600250110ustar00rootroot00000000000000{ "apiVer": "4.5.0", "hwVer": 3, "swVer": "4.0.925" } regenmaschine-2024.03.0/tests/fixtures/auth_login_response.json000066400000000000000000000002321457362162600246200ustar00rootroot00000000000000{ "access_token": "12345abcdef", "checksum": "98765", "expires_in": 157680000, "expiration": "Tue, 06 Jun 2023 02:17:46 GMT", "statusCode": 0 } regenmaschine-2024.03.0/tests/fixtures/auth_totp_response.json000066400000000000000000000000251457362162600244760ustar00rootroot00000000000000{ "totp": 123456 } regenmaschine-2024.03.0/tests/fixtures/dailystats_date_response.json000066400000000000000000000004461457362162600256540ustar00rootroot00000000000000{ "id": 0, "day": "2018-06-04", "mint": 12.78, "maxt": 33.33, "icon": 2, "percentage": 100, "wateringFlag": 0, "vibration": [100, 100, 100, 100, 100, 100, 100, 100, 100, 100], "simulatedPercentage": 100, "simulatedVibration": [100, 100, 100, 100, 100, 100, 100, 100, 100] } regenmaschine-2024.03.0/tests/fixtures/dailystats_details_response.json000066400000000000000000000274301457362162600263660ustar00rootroot00000000000000{ "DailyStatsDetails": [ { "dayTimestamp": 1528092000, "day": "2018-06-04", "mint": 12.78, "maxt": 33.33, "icon": 2, "programs": [ { "id": 1, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] }, { "id": 2, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] }, { "id": 3, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] }, { "id": 4, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] } ], "simulatedPrograms": [ { "id": 1, "zones": [ { "id": 2, "scheduledWateringTime": 300, "computedWateringTime": 300, "availableWater": 0, "coefficient": 1, "percentage": 100 } ] } ] }, { "dayTimestamp": 1528178400, "day": "2018-06-05", "mint": 13.89, "maxt": 34.44, "icon": 3, "programs": [ { "id": 1, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] }, { "id": 2, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] }, { "id": 3, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] }, { "id": 4, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] } ], "simulatedPrograms": [ { "id": 1, "zones": [ { "id": 2, "scheduledWateringTime": 300, "computedWateringTime": 300, "availableWater": 0, "coefficient": 1, "percentage": 100 } ] } ] }, { "dayTimestamp": 1528264800, "day": "2018-06-06", "mint": 14.44, "maxt": 32.78, "icon": 3, "programs": [ { "id": 1, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] }, { "id": 2, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] } ], "simulatedPrograms": [ { "id": 1, "zones": [ { "id": 2, "scheduledWateringTime": 300, "computedWateringTime": 300, "availableWater": 0, "coefficient": 1, "percentage": 100 } ] } ] }, { "dayTimestamp": 1528351200, "day": "2018-06-07", "mint": 13.89, "maxt": 33.89, "icon": 3, "programs": [ { "id": 1, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] }, { "id": 2, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] } ], "simulatedPrograms": [ { "id": 1, "zones": [ { "id": 2, "scheduledWateringTime": 300, "computedWateringTime": 300, "availableWater": 0, "coefficient": 1, "percentage": 100 } ] } ] }, { "dayTimestamp": 1528437600, "day": "2018-06-08", "mint": 13.89, "maxt": 34.44, "icon": 3, "programs": [ { "id": 1, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] }, { "id": 2, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] } ], "simulatedPrograms": [ { "id": 1, "zones": [ { "id": 2, "scheduledWateringTime": 300, "computedWateringTime": 300, "availableWater": 0, "coefficient": 1, "percentage": 100 } ] } ] }, { "dayTimestamp": 1528524000, "day": "2018-06-09", "mint": 14.44, "maxt": 33.89, "icon": 3, "programs": [ { "id": 1, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] }, { "id": 2, "zones": [ { "id": 1, "scheduledWateringTime": 1243, "computedWateringTime": 1243, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 }, { "id": 2, "scheduledWateringTime": 2680, "computedWateringTime": 2680, "availableWater": 0, "coefficient": 1, "percentage": 100, "wateringFlag": 0 } ] } ], "simulatedPrograms": [ { "id": 1, "zones": [ { "id": 2, "scheduledWateringTime": 300, "computedWateringTime": 300, "availableWater": 0, "coefficient": 1, "percentage": 100 } ] } ] }, { "dayTimestamp": 1528610400, "day": "2018-06-10", "mint": null, "maxt": null, "icon": null, "programs": [], "simulatedPrograms": [] } ] } regenmaschine-2024.03.0/tests/fixtures/diag_log_response.json000066400000000000000000000000241457362162600242330ustar00rootroot00000000000000{ "log": "----" } regenmaschine-2024.03.0/tests/fixtures/diag_response.json000066400000000000000000000007651457362162600234060ustar00rootroot00000000000000{ "hasWifi": true, "uptime": "14 days, 8:45:19", "uptimeSeconds": 1241119, "memUsage": 18220, "networkStatus": true, "bootCompleted": true, "lastCheckTimestamp": 1527997722, "wizardHasRun": true, "standaloneMode": false, "cpuUsage": 0, "lastCheck": "2018-06-02 21:48:42", "softwareVersion": "4.0.925", "internetStatus": true, "locationStatus": true, "timeStatus": true, "wifiMode": null, "gatewayAddress": "192.168.1.1", "cloudStatus": 0, "weatherStatus": true } regenmaschine-2024.03.0/tests/fixtures/machine_check_update_response.json000066400000000000000000000000511457362162600265710ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/fixtures/machine_get_update_response.json000066400000000000000000000006201457362162600262750ustar00rootroot00000000000000{ "lastUpdateCheckTimestamp": 1657825288, "packageDetails": [ { "packageName": "rainmachine-app", "newVersion": "4.0.1036", "oldVersion": "4.0.1003" }, { "packageName": "rainmachine-web-ui", "newVersion": "2021-10-13-28", "oldVersion": "2020-07-31-24" } ], "update": false, "lastUpdateCheck": "2022-07-14 13:01:28", "updateStatus": 1 } regenmaschine-2024.03.0/tests/fixtures/machine_reboot_response.json000066400000000000000000000000511457362162600254440ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/fixtures/machine_update_response.json000066400000000000000000000000511457362162600254340ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/fixtures/parser_post_data_payload.json000066400000000000000000000004571457362162600256250ustar00rootroot00000000000000{ "weather": [ { "mintemp": null, "maxtemp": null, "temperature": null, "wind": null, "solarrad": null, "qpf": null, "rain": null, "minrh": null, "maxrh": null, "condition": 26, "pressure": null, "dewpoint": null } ] } regenmaschine-2024.03.0/tests/fixtures/parser_post_data_response.json000066400000000000000000000000511457362162600260200ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/fixtures/parser_response.json000066400000000000000000000005141457362162600237660ustar00rootroot00000000000000{ "parsers": [ { "lastRun": "2018-04-30 11:52:33", "lastKnownError": "", "hasForecast": true, "uid": 11, "hasHistorical": false, "description": "North America weather forecast", "enabled": true, "custom": false, "isRunning": false, "name": "NOAA Parser" } ] } regenmaschine-2024.03.0/tests/fixtures/program_id_response.json000066400000000000000000000043401457362162600246160ustar00rootroot00000000000000{ "uid": 1, "name": "Morning", "active": true, "startTime": "06:00", "cycles": 0, "soak": 0, "cs_on": false, "delay": 0, "delay_on": false, "status": 0, "startTimeParams": { "offsetSign": 0, "type": 0, "offsetMinutes": 0 }, "frequency": { "type": 0, "param": "0" }, "coef": 0, "ignoreInternetWeather": false, "futureField1": 0, "freq_modified": 0, "useWaterSense": false, "nextRun": "2018-04-27", "endDate": "1969-12-31", "simulationExpired": false, "yearlyRecurring": true, "wateringTimes": [ { "id": 1, "name": "Landscaping", "duration": 0, "active": true, "userPercentage": 1, "order": -1 }, { "id": 2, "name": "Flower Box", "duration": 0, "active": true, "userPercentage": 1, "order": -1 }, { "id": 3, "name": "Zone 3", "duration": 0, "active": false, "userPercentage": 1, "order": -1 }, { "id": 4, "name": "Zone 4", "duration": 0, "active": false, "userPercentage": 1, "order": -1 }, { "id": 5, "name": "Zone 5", "duration": 0, "active": false, "userPercentage": 1, "order": -1 }, { "id": 6, "name": "Zone 6", "duration": 0, "active": false, "userPercentage": 1, "order": -1 }, { "id": 7, "name": "Zone 7", "duration": 0, "active": false, "userPercentage": 1, "order": -1 }, { "id": 8, "name": "Zone 8", "duration": 0, "active": false, "userPercentage": 1, "order": -1 }, { "id": 9, "name": "Zone 9", "duration": 0, "active": false, "userPercentage": 1, "order": -1 }, { "id": 10, "name": "Zone 10", "duration": 0, "active": false, "userPercentage": 1, "order": -1 }, { "id": 11, "name": "Zone 11", "duration": 0, "active": false, "userPercentage": 1, "order": -1 }, { "id": 12, "name": "Zone 12", "duration": 0, "active": false, "userPercentage": 1, "order": -1 } ] } regenmaschine-2024.03.0/tests/fixtures/program_nextrun_response.json000066400000000000000000000002071457362162600257230ustar00rootroot00000000000000{ "nextRuns": [ { "pid": 1, "startTime": "06:00" }, { "pid": 2, "startTime": "22:00" } ] } regenmaschine-2024.03.0/tests/fixtures/program_post_response.json000066400000000000000000000000511457362162600252020ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/fixtures/program_response.json000066400000000000000000000143601457362162600241450ustar00rootroot00000000000000{ "programs": [ { "uid": 1, "name": "Morning", "active": true, "startTime": "06:00", "cycles": 0, "soak": 0, "cs_on": false, "delay": 0, "delay_on": false, "status": 0, "startTimeParams": { "offsetSign": 0, "type": 0, "offsetMinutes": 0 }, "frequency": { "type": 0, "param": "0" }, "coef": 0, "ignoreInternetWeather": false, "futureField1": 0, "freq_modified": 0, "useWaterSense": false, "nextRun": "2018-06-04", "startDate": "2018-04-28", "endDate": null, "yearlyRecurring": true, "simulationExpired": false, "wateringTimes": [ { "id": 1, "order": -1, "name": "Landscaping", "duration": 0, "active": true, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 2, "order": -1, "name": "Flower Box", "duration": 0, "active": true, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 3, "order": -1, "name": "TEST", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 4, "order": -1, "name": "Zone 4", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 5, "order": -1, "name": "Zone 5", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 6, "order": -1, "name": "Zone 6", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 7, "order": -1, "name": "Zone 7", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 8, "order": -1, "name": "Zone 8", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 9, "order": -1, "name": "Zone 9", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 10, "order": -1, "name": "Zone 10", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 11, "order": -1, "name": "Zone 11", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 12, "order": -1, "name": "Zone 12", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 } ] }, { "uid": 2, "name": "Evening", "active": false, "startTime": "06:00", "cycles": 0, "soak": 0, "cs_on": false, "delay": 0, "delay_on": false, "status": 0, "startTimeParams": { "offsetSign": 0, "type": 0, "offsetMinutes": 0 }, "frequency": { "type": 0, "param": "0" }, "coef": 0, "ignoreInternetWeather": false, "futureField1": 0, "freq_modified": 0, "useWaterSense": false, "nextRun": "2018-06-04", "startDate": "2018-04-28", "endDate": null, "yearlyRecurring": true, "simulationExpired": false, "wateringTimes": [ { "id": 1, "order": -1, "name": "Landscaping", "duration": 0, "active": true, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 2, "order": -1, "name": "Flower Box", "duration": 0, "active": true, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 3, "order": -1, "name": "TEST", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 4, "order": -1, "name": "Zone 4", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 5, "order": -1, "name": "Zone 5", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 6, "order": -1, "name": "Zone 6", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 7, "order": -1, "name": "Zone 7", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 8, "order": -1, "name": "Zone 8", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 9, "order": -1, "name": "Zone 9", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 10, "order": -1, "name": "Zone 10", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 11, "order": -1, "name": "Zone 11", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 }, { "id": 12, "order": -1, "name": "Zone 12", "duration": 0, "active": false, "userPercentage": 1, "minRuntimeCoef": 1 } ] } ] } regenmaschine-2024.03.0/tests/fixtures/program_start_stop_response.json000066400000000000000000000000511457362162600264170ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/fixtures/provision_name_response.json000066400000000000000000000000311457362162600255140ustar00rootroot00000000000000{ "name": "My House" } regenmaschine-2024.03.0/tests/fixtures/provision_response.json000066400000000000000000000040661457362162600245300ustar00rootroot00000000000000{ "system": { "httpEnabled": true, "rainSensorSnoozeDuration": 0, "uiUnitsMetric": false, "programZonesShowInactive": false, "programSingleSchedule": false, "standaloneMode": false, "masterValveAfter": 0, "touchSleepTimeout": 10, "selfTest": false, "useSoftwareRainSensor": false, "defaultZoneWateringDuration": 300, "maxLEDBrightness": 40, "simulatorHistorySize": 0, "vibration": false, "masterValveBefore": 0, "touchProgramToRun": null, "useRainSensor": false, "wizardHasRun": true, "waterLogHistorySize": 365, "netName": "Home", "softwareRainSensorMinQPF": 5, "touchAdvanced": false, "useBonjourService": true, "hardwareVersion": 3, "touchLongPressTimeout": 3, "showRestrictionsOnLed": false, "parserDataSizeInDays": 6, "programListShowInactive": true, "parserHistorySize": 365, "allowAlexaDiscovery": false, "automaticUpdates": true, "minLEDBrightness": 0, "minWateringDurationThreshold": 0, "localValveCount": 12, "touchAuthAPSeconds": 60, "useCommandLineArguments": false, "databasePath": "/rainmachine-app/DB/Default", "touchCyclePrograms": true, "zoneListShowInactive": true, "rainSensorRainStart": null, "zoneDuration": [ 300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300, 300 ], "rainSensorIsNormallyClosed": true, "useCorrectionForPast": true, "useMasterValve": false, "runParsersBeforePrograms": true, "maxWateringCoef": 2, "mixerHistorySize": 365 }, "location": { "elevation": 1593.45141602, "doyDownloaded": true, "zip": null, "windSensitivity": 0.5, "krs": 0.16, "stationID": 9172, "stationSource": "station", "et0Average": 6.578, "latitude": 21.037234682342, "state": "Default", "stationName": "MY STATION", "wsDays": 2, "stationDownloaded": true, "address": "Default", "rainSensitivity": 0.8, "timezone": "America/Los Angeles", "longitude": -87.12872612, "name": "123 Main Street, Boston, MA 01234" } } regenmaschine-2024.03.0/tests/fixtures/provision_wifi_response.json000066400000000000000000000003341457362162600255400ustar00rootroot00000000000000{ "macAddress": "ab:cd:ef:12:34:56", "ssid": null, "netmaskAddress": null, "hasClientLink": false, "mode": null, "interface": "wlan0", "lastWIFICheckTimestamp": 1525126177, "ipAddress": "192.168.1.100" } regenmaschine-2024.03.0/tests/fixtures/remote_auth_login_1_response.json000066400000000000000000000000671457362162600264210ustar00rootroot00000000000000{ "access_token": "12345abcdef", "errorType": -1 } regenmaschine-2024.03.0/tests/fixtures/remote_auth_login_2_response.json000066400000000000000000000001251457362162600264150ustar00rootroot00000000000000{ "access_token": "12345abcdef", "errorType": 0, "sprinklerId": "12345abcde" } regenmaschine-2024.03.0/tests/fixtures/remote_error_http_body_response.json000066400000000000000000000001041457362162600272450ustar00rootroot00000000000000{ "statusCode": 400, "error": 400, "message": "Bad Request" } regenmaschine-2024.03.0/tests/fixtures/remote_error_known_response.json000066400000000000000000000000251457362162600264070ustar00rootroot00000000000000{ "errorType": 1 } regenmaschine-2024.03.0/tests/fixtures/remote_error_unknown_response.json000066400000000000000000000000271457362162600267540ustar00rootroot00000000000000{ "errorType": 999 } regenmaschine-2024.03.0/tests/fixtures/remote_sprinklers_response.json000066400000000000000000000004071457362162600262420ustar00rootroot00000000000000{ "errorType": 0, "sprinklers": [ { "sprinklerId": "12345abcde", "sprinklerUrl": "https://api.rainmachine.com/12345abcde/", "mac": "ab:cd:ef:12:34:56", "name": "My House", "type": "SPK3", "swVer": "4.0.974" } ] } regenmaschine-2024.03.0/tests/fixtures/restrictions_currently_response.json000066400000000000000000000002261457362162600273310ustar00rootroot00000000000000{ "hourly": false, "freeze": false, "month": false, "weekDay": false, "rainDelay": false, "rainDelayCounter": -1, "rainSensor": false } regenmaschine-2024.03.0/tests/fixtures/restrictions_global_response.json000066400000000000000000000003431457362162600265420ustar00rootroot00000000000000{ "hotDaysExtraWatering": false, "freezeProtectEnabled": true, "freezeProtectTemp": 2, "noWaterInWeekDays": "0000000", "noWaterInMonths": "000000000000", "rainDelayStartTime": 1524854551, "rainDelayDuration": 0 } regenmaschine-2024.03.0/tests/fixtures/restrictions_global_set_response.json000066400000000000000000000000511457362162600274110ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/fixtures/restrictions_hourly_response.json000066400000000000000000000000371457362162600266240ustar00rootroot00000000000000{ "hourlyRestrictions": [] } regenmaschine-2024.03.0/tests/fixtures/restrictions_raindelay_response.json000066400000000000000000000000311457362162600272440ustar00rootroot00000000000000{ "delayCounter": -1 } regenmaschine-2024.03.0/tests/fixtures/unauthenticated_response.json000066400000000000000000000000721457362162600256560ustar00rootroot00000000000000{ "statusCode": 2, "message": "Not Authenticated !" } regenmaschine-2024.03.0/tests/fixtures/unknown_api_call_response.json000066400000000000000000000000701457362162600260120ustar00rootroot00000000000000{ "statusCode": 13, "message": "API call unknown" } regenmaschine-2024.03.0/tests/fixtures/watering_flowmeter_post_response.json000066400000000000000000000000261457362162600274410ustar00rootroot00000000000000{ "statusCode": 0 } regenmaschine-2024.03.0/tests/fixtures/watering_flowmeter_response.json000066400000000000000000000001471457362162600264000ustar00rootroot00000000000000{ "flowMeterWateringClicks": 4000, "flowMeterLeakClicks": 10, "flowMeterStartIndexClicks": 120 } regenmaschine-2024.03.0/tests/fixtures/watering_log_response.json000066400000000000000000000137631457362162600251650ustar00rootroot00000000000000{ "waterLog": { "days": [ { "date": "2018-06-01", "dateTimestamp": 1527832800, "programs": [ { "id": 1, "zones": [ { "uid": 1, "flag": 0, "cycles": [ { "id": 1, "startTime": "2018-06-01 06:00:02", "startTimestamp": 1527854402, "userDuration": 1243, "machineDuration": 1243, "realDuration": 1243 } ] }, { "uid": 2, "flag": 0, "cycles": [ { "id": 1, "startTime": "2018-06-01 06:20:45", "startTimestamp": 1527855645, "userDuration": 2680, "machineDuration": 2680, "realDuration": 2680 } ] } ] }, { "id": 2, "zones": [ { "uid": 1, "flag": 0, "cycles": [ { "id": 1, "startTime": "2018-06-01 22:00:01", "startTimestamp": 1527912001, "userDuration": 1243, "machineDuration": 1243, "realDuration": 1243 } ] }, { "uid": 2, "flag": 0, "cycles": [ { "id": 1, "startTime": "2018-06-01 22:20:44", "startTimestamp": 1527913244, "userDuration": 2680, "machineDuration": 2680, "realDuration": 2680 } ] } ] } ] }, { "date": "2018-06-02", "dateTimestamp": 1527919200, "programs": [ { "id": 1, "zones": [ { "uid": 1, "flag": 0, "cycles": [ { "id": 1, "startTime": "2018-06-02 06:00:01", "startTimestamp": 1527940801, "userDuration": 1243, "machineDuration": 1217, "realDuration": 1217 } ] }, { "uid": 2, "flag": 0, "cycles": [ { "id": 1, "startTime": "2018-06-02 06:20:18", "startTimestamp": 1527942018, "userDuration": 2680, "machineDuration": 2624, "realDuration": 2624 } ] } ] }, { "id": 0, "zones": [ { "uid": 1, "flag": 1, "cycles": [ { "id": 1, "startTime": "2018-06-02 20:58:54", "startTimestamp": 1527994734, "userDuration": 1243, "machineDuration": 1243, "realDuration": 6 } ] } ] }, { "id": 0, "zones": [ { "uid": 1, "flag": 1, "cycles": [ { "id": 1, "startTime": "2018-06-02 21:00:40", "startTimestamp": 1527994840, "userDuration": 1243, "machineDuration": 1243, "realDuration": 3 } ] } ] }, { "id": 0, "zones": [ { "uid": 1, "flag": 1, "cycles": [ { "id": 1, "startTime": "2018-06-02 21:20:03", "startTimestamp": 1527996003, "userDuration": 1243, "machineDuration": 1243, "realDuration": 3 } ] } ] }, { "id": 0, "zones": [ { "uid": 1, "flag": 1, "cycles": [ { "id": 1, "startTime": "2018-06-02 21:40:41", "startTimestamp": 1527997241, "userDuration": 3, "machineDuration": 3, "realDuration": 2 } ] } ] }, { "id": 2, "zones": [ { "uid": 1, "flag": 0, "cycles": [ { "id": 1, "startTime": "2018-06-02 22:00:02", "startTimestamp": 1527998402, "userDuration": 1243, "machineDuration": 1180, "realDuration": 1180 } ] }, { "uid": 2, "flag": 0, "cycles": [ { "id": 1, "startTime": "2018-06-02 22:19:42", "startTimestamp": 1527999582, "userDuration": 2680, "machineDuration": 2545, "realDuration": 2545 } ] } ] } ] } ] } } regenmaschine-2024.03.0/tests/fixtures/watering_past_response.json000066400000000000000000000026041457362162600253430ustar00rootroot00000000000000{ "pastValues": [ { "pid": 3, "dateTimestamp": 1528005600, "dateTime": "2018-06-03 00:00:00", "used": false, "et0": 6.025417130938126, "qpf": 0 }, { "pid": 4, "dateTimestamp": 1528005600, "dateTime": "2018-06-03 00:00:00", "used": false, "et0": 6.025417130938126, "qpf": 0 }, { "pid": 3, "dateTimestamp": 1528092000, "dateTime": "2018-06-04 00:00:00", "used": false, "et0": 7.1343372845726805, "qpf": 0 }, { "pid": 4, "dateTimestamp": 1528092000, "dateTime": "2018-06-04 00:00:00", "used": false, "et0": 7.1343372845726805, "qpf": 0 }, { "pid": 1, "dateTimestamp": 1528005600, "dateTime": "2018-06-03 00:00:00", "used": true, "et0": 6.010685943453997, "qpf": 0.13 }, { "pid": 2, "dateTimestamp": 1528005600, "dateTime": "2018-06-03 00:00:00", "used": true, "et0": 5.612352387321412, "qpf": 0 }, { "pid": 1, "dateTimestamp": 1528092000, "dateTime": "2018-06-04 00:00:00", "used": true, "et0": 7.2596885196807355, "qpf": 0 }, { "pid": 2, "dateTimestamp": 1528092000, "dateTime": "2018-06-04 00:00:00", "used": false, "et0": 7.356361013751831, "qpf": 0 } ] } regenmaschine-2024.03.0/tests/fixtures/watering_pause_response.json000066400000000000000000000000511457362162600255030ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/fixtures/watering_program_response.json000066400000000000000000000003221457362162600260360ustar00rootroot00000000000000{ "programs": [ { "uid": 2, "name": "Evening", "manual": false, "userStartTime": "2018-06-02 22:00:00", "realStartTime": "2018-06-02 22:00:02", "status": 1 } ] } regenmaschine-2024.03.0/tests/fixtures/watering_queue_response.json000066400000000000000000000000221457362162600255100ustar00rootroot00000000000000{ "queue": [] } regenmaschine-2024.03.0/tests/fixtures/watering_stopall_response.json000066400000000000000000000000511457362162600260440ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/fixtures/watering_zone_response.json000066400000000000000000000050421457362162600253460ustar00rootroot00000000000000{ "zones": [ { "uid": 1, "name": "Zone 1", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 2, "name": "Zone 2", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 3, "name": "Zone 3", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 4, "name": "Zone 4", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 5, "name": "Zone 5", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 6, "name": "Zone 6", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 7, "name": "Zone 7", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 8, "name": "Zone 8", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 9, "name": "Zone 9", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 10, "name": "Zone 10", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 11, "name": "Zone 11", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false }, { "uid": 12, "name": "Zone 12", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false } ] } regenmaschine-2024.03.0/tests/fixtures/zone_id_properties_response.json000066400000000000000000000014561457362162600264030ustar00rootroot00000000000000{ "uid": 1, "name": "Landscaping", "valveid": 1, "ETcoef": 0.8, "active": true, "type": 4, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 5, "group_id": 4, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.17, "rootDepth": 229, "minRuntime": -1, "appEfficiency": 0.75, "isTallPlant": true, "permWilting": 0.03, "allowedSurfaceAcc": 8.38, "maxAllowedDepletion": 0.5, "precipitationRate": 25.4, "currentFieldCapacity": 16.03, "area": 92.9000015258789, "referenceTime": 1243, "detailedMonthsKc": [1, 1, 1, 1, 1, 1, 1], "flowrate": null, "soilIntakeRate": 10.16 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null } regenmaschine-2024.03.0/tests/fixtures/zone_id_response.json000066400000000000000000000003671457362162600241270ustar00rootroot00000000000000{ "uid": 1, "name": "Landscaping", "state": 0, "active": true, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 4, "master": false, "waterSense": false } regenmaschine-2024.03.0/tests/fixtures/zone_post_response.json000066400000000000000000000000511457362162600245060ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/fixtures/zone_properties_response.json000066400000000000000000000330141457362162600257220ustar00rootroot00000000000000{ "zones": [ { "uid": 1, "name": "Landscaping", "valveid": 1, "ETcoef": 0.80000000000000004, "active": true, "type": 4, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 5, "group_id": 4, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.17000000000000001, "rootDepth": 229, "minRuntime": 0, "appEfficiency": 0.75, "isTallPlant": true, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 8.3800000000000008, "maxAllowedDepletion": 0.5, "precipitationRate": 25.399999999999999, "currentFieldCapacity": 16.030000000000001, "area": 92.900001525878906, "referenceTime": 1243, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": null, "soilIntakeRate": 10.16 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 2, "name": "Flower Box", "valveid": 2, "ETcoef": 0.80000000000000004, "active": true, "type": 5, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 5, "group_id": 3, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.17000000000000001, "rootDepth": 457, "minRuntime": 5, "appEfficiency": 0.80000000000000004, "isTallPlant": true, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 8.3800000000000008, "maxAllowedDepletion": 0.34999999999999998, "precipitationRate": 12.699999999999999, "currentFieldCapacity": 22.390000000000001, "area": 92.900000000000006, "referenceTime": 2680, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": null, "soilIntakeRate": 10.16 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 3, "name": "TEST", "valveid": 3, "ETcoef": 0.80000000000000004, "active": false, "type": 9, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 1, "group_id": 1, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.29999999999999999, "rootDepth": 700, "minRuntime": 0, "appEfficiency": 0.69999999999999996, "isTallPlant": true, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 6.5999999999999996, "maxAllowedDepletion": 0.59999999999999998, "precipitationRate": 35.560000000000002, "currentFieldCapacity": 113.40000000000001, "area": 92.900000000000006, "referenceTime": 380, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": null, "soilIntakeRate": 5.0800000000000001 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 4, "name": "Zone 4", "valveid": 4, "ETcoef": 0.80000000000000004, "active": false, "type": 2, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 1, "group_id": 1, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.29999999999999999, "rootDepth": 203, "minRuntime": -1, "appEfficiency": 0.69999999999999996, "isTallPlant": false, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 6.5999999999999996, "maxAllowedDepletion": 0.40000000000000002, "precipitationRate": 35.560000000000002, "currentFieldCapacity": 21.920000000000002, "area": 92.900000000000006, "referenceTime": 761, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": 0.0, "soilIntakeRate": 5.0800000000000001 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 5, "name": "Zone 5", "valveid": 5, "ETcoef": 0.80000000000000004, "active": false, "type": 2, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 1, "group_id": 1, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.29999999999999999, "rootDepth": 203, "minRuntime": -1, "appEfficiency": 0.69999999999999996, "isTallPlant": false, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 6.5999999999999996, "maxAllowedDepletion": 0.40000000000000002, "precipitationRate": 35.560000000000002, "currentFieldCapacity": 21.920000000000002, "area": 92.900000000000006, "referenceTime": 761, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": 0.0, "soilIntakeRate": 5.0800000000000001 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 6, "name": "Zone 6", "valveid": 6, "ETcoef": 0.80000000000000004, "active": false, "type": 2, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 1, "group_id": 1, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.29999999999999999, "rootDepth": 203, "minRuntime": -1, "appEfficiency": 0.69999999999999996, "isTallPlant": false, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 6.5999999999999996, "maxAllowedDepletion": 0.40000000000000002, "precipitationRate": 35.560000000000002, "currentFieldCapacity": 21.920000000000002, "area": 92.900000000000006, "referenceTime": 761, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": 0.0, "soilIntakeRate": 5.0800000000000001 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 7, "name": "Zone 7", "valveid": 7, "ETcoef": 0.80000000000000004, "active": false, "type": 2, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 1, "group_id": 1, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.29999999999999999, "rootDepth": 203, "minRuntime": -1, "appEfficiency": 0.69999999999999996, "isTallPlant": false, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 6.5999999999999996, "maxAllowedDepletion": 0.40000000000000002, "precipitationRate": 35.560000000000002, "currentFieldCapacity": 21.920000000000002, "area": 92.900000000000006, "referenceTime": 761, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": 0.0, "soilIntakeRate": 5.0800000000000001 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 8, "name": "Zone 8", "valveid": 8, "ETcoef": 0.80000000000000004, "active": false, "type": 2, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 1, "group_id": 1, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.29999999999999999, "rootDepth": 203, "minRuntime": -1, "appEfficiency": 0.69999999999999996, "isTallPlant": false, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 6.5999999999999996, "maxAllowedDepletion": 0.40000000000000002, "precipitationRate": 35.560000000000002, "currentFieldCapacity": 21.920000000000002, "area": 92.900000000000006, "referenceTime": 761, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": 0.0, "soilIntakeRate": 5.0800000000000001 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 9, "name": "Zone 9", "valveid": 9, "ETcoef": 0.80000000000000004, "active": false, "type": 2, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 1, "group_id": 1, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.29999999999999999, "rootDepth": 203, "minRuntime": -1, "appEfficiency": 0.69999999999999996, "isTallPlant": false, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 6.5999999999999996, "maxAllowedDepletion": 0.40000000000000002, "precipitationRate": 35.560000000000002, "currentFieldCapacity": 21.920000000000002, "area": 92.900000000000006, "referenceTime": 761, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": 0.0, "soilIntakeRate": 5.0800000000000001 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 10, "name": "Zone 10", "valveid": 10, "ETcoef": 0.80000000000000004, "active": false, "type": 2, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 1, "group_id": 1, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.29999999999999999, "rootDepth": 203, "minRuntime": -1, "appEfficiency": 0.69999999999999996, "isTallPlant": false, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 6.5999999999999996, "maxAllowedDepletion": 0.40000000000000002, "precipitationRate": 35.560000000000002, "currentFieldCapacity": 21.920000000000002, "area": 92.900000000000006, "referenceTime": 761, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": 0.0, "soilIntakeRate": 5.0800000000000001 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 11, "name": "Zone 11", "valveid": 11, "ETcoef": 0.80000000000000004, "active": false, "type": 2, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 1, "group_id": 1, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.29999999999999999, "rootDepth": 203, "minRuntime": -1, "appEfficiency": 0.69999999999999996, "isTallPlant": false, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 6.5999999999999996, "maxAllowedDepletion": 0.40000000000000002, "precipitationRate": 35.560000000000002, "currentFieldCapacity": 21.920000000000002, "area": 92.900000000000006, "referenceTime": 761, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": 0.0, "soilIntakeRate": 5.0800000000000001 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null }, { "uid": 12, "name": "Zone 12", "valveid": 12, "ETcoef": 0.80000000000000004, "active": false, "type": 2, "internet": true, "savings": 100, "slope": 1, "sun": 1, "soil": 1, "group_id": 1, "history": true, "master": false, "before": 0, "after": 0, "waterSense": { "fieldCapacity": 0.29999999999999999, "rootDepth": 203, "minRuntime": -1, "appEfficiency": 0.69999999999999996, "isTallPlant": false, "permWilting": 0.029999999999999999, "allowedSurfaceAcc": 6.5999999999999996, "maxAllowedDepletion": 0.40000000000000002, "precipitationRate": 35.560000000000002, "currentFieldCapacity": 21.920000000000002, "area": 92.900000000000006, "referenceTime": 761, "detailedMonthsKc": [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ], "flowrate": 0.0, "soilIntakeRate": 5.0800000000000001 }, "customSoilPreset": null, "customVegetationPreset": null, "customSprinklerPreset": null } ] } regenmaschine-2024.03.0/tests/fixtures/zone_response.json000066400000000000000000000071371457362162600234550ustar00rootroot00000000000000{ "zones": [ { "uid": 1, "name": "Landscaping", "state": 0, "active": true, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 4, "master": false, "waterSense": false }, { "uid": 2, "name": "Flower Box", "state": 0, "active": true, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 5, "master": false, "waterSense": false }, { "uid": 3, "name": "TEST", "state": 0, "active": false, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 9, "master": false, "waterSense": false }, { "uid": 4, "name": "Zone 4", "state": 0, "active": false, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 5, "name": "Zone 5", "state": 0, "active": false, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 6, "name": "Zone 6", "state": 0, "active": false, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 7, "name": "Zone 7", "state": 0, "active": false, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 8, "name": "Zone 8", "state": 0, "active": false, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 9, "name": "Zone 9", "state": 0, "active": false, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 10, "name": "Zone 10", "state": 0, "active": false, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 11, "name": "Zone 11", "state": 0, "active": false, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 12, "name": "Zone 12", "state": 0, "active": false, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false } ] } regenmaschine-2024.03.0/tests/fixtures/zone_response_gen1.json000066400000000000000000000065151457362162600243660ustar00rootroot00000000000000{ "zones": [ { "uid": 1, "name": "Landscaping", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 4, "master": false, "waterSense": false }, { "uid": 2, "name": "Flower Box", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 5, "master": false, "waterSense": false }, { "uid": 3, "name": "TEST", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 9, "master": false, "waterSense": false }, { "uid": 4, "name": "Zone 4", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 5, "name": "Zone 5", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 6, "name": "Zone 6", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 7, "name": "Zone 7", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 8, "name": "Zone 8", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 9, "name": "Zone 9", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 10, "name": "Zone 10", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 11, "name": "Zone 11", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false }, { "uid": 12, "name": "Zone 12", "state": 0, "userDuration": 0, "machineDuration": 0, "remaining": 0, "cycle": 0, "noOfCycles": 0, "restriction": false, "type": 2, "master": false, "waterSense": false } ] } regenmaschine-2024.03.0/tests/fixtures/zone_start_stop_response.json000066400000000000000000000000511457362162600257230ustar00rootroot00000000000000{ "statusCode": 0, "message": "OK" } regenmaschine-2024.03.0/tests/test_client.py000066400000000000000000000401611457362162600207010ustar00rootroot00000000000000"""Define tests for the client object.""" # pylint: disable=protected-access import asyncio import json from datetime import datetime, timedelta from typing import Any from unittest.mock import Mock, patch import aiohttp import pytest from aresponses import ResponsesMockServer from regenmaschine import Client from regenmaschine.errors import ( RainMachineError, RequestError, TokenExpiredError, UnknownAPICallError, ) from tests.common import ( TEST_ACCESS_TOKEN, TEST_API_VERSION, TEST_EMAIL, TEST_HOST, TEST_HW_VERSION, TEST_MAC, TEST_NAME, TEST_PASSWORD, TEST_PORT, TEST_SW_VERSION, load_fixture, ) @pytest.mark.asyncio async def test_legacy_login( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test loading a local controller through the legacy method. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client, aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local(TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False) controller = next(iter(client.controllers.values())) assert controller._access_token == TEST_ACCESS_TOKEN assert controller.api_version == TEST_API_VERSION assert controller.hardware_version == TEST_HW_VERSION assert controller.mac == TEST_MAC assert controller.name == TEST_NAME assert controller.software_version == TEST_SW_VERSION aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_load_local( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test loading a local controller. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client, aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local(TEST_HOST, TEST_PASSWORD, TEST_PORT, False) assert len(client.controllers) == 1 controller = client.controllers[TEST_MAC] assert controller._access_token == TEST_ACCESS_TOKEN assert controller.api_version == TEST_API_VERSION assert controller.hardware_version == TEST_HW_VERSION assert controller.mac == TEST_MAC assert controller.name == TEST_NAME assert controller.software_version == TEST_SW_VERSION aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio @pytest.mark.parametrize("provision_name_response", [{"name": "89"}]) async def test_load_local_string_name( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test loading a local controller whose name (per the API) is not a string. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client, aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local(TEST_HOST, TEST_PASSWORD, TEST_PORT, False) assert len(client.controllers) == 1 controller = client.controllers[TEST_MAC] assert isinstance(controller.name, str) assert controller.name == "89" aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_load_local_skip( aresponses: ResponsesMockServer, auth_login_response: dict[str, Any], authenticated_local_client: ResponsesMockServer, provision_wifi_response: dict[str, Any], ) -> None: """Test skipping the loading of a local controller if it's already loaded. Args: aresponses: An aresponses server. auth_login_response: An API response payload. authenticated_local_client: A mock local controller. provision_wifi_response: An API response payload. """ authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/auth/login", "post", response=aiohttp.web_response.json_response(auth_login_response, status=200), ) authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/provision/wifi", "get", response=aiohttp.web_response.json_response( provision_wifi_response, status=200 ), ) async with authenticated_local_client, aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local(TEST_HOST, TEST_PASSWORD, TEST_PORT, True) controller = client.controllers[TEST_MAC] await client.load_local(TEST_HOST, TEST_PASSWORD, TEST_PORT, True) assert len(client.controllers) == 1 assert client.controllers[TEST_MAC] == controller aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_load_local_http_error(aresponses: ResponsesMockServer) -> None: """Test loading a local controller and receiving a fail response. Args: aresponses: An aresponses server. """ aresponses.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/auth/login", "post", aresponses.Response(text=None, status=500), ) with pytest.raises(RequestError): async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local(TEST_HOST, TEST_PASSWORD, TEST_PORT, False) aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio @pytest.mark.parametrize( "exc", [ aiohttp.client_exceptions.ClientConnectorError(Mock(), Mock()), asyncio.TimeoutError(), json.decoder.JSONDecodeError("Not valid", "", 0), ], ) async def test_load_local_other_errors(exc: type[Exception]) -> None: """Test loading a local controller and encountering a non-HTTP issue.""" async with aiohttp.ClientSession() as session: client = Client(session=session) with patch.object(client._session, "request", side_effect=exc), pytest.raises( RequestError ): await client.load_local(TEST_HOST, TEST_PASSWORD, TEST_PORT, False) @pytest.mark.asyncio async def test_load_remote( aresponses: ResponsesMockServer, authenticated_remote_client: ResponsesMockServer ) -> None: """Test loading a remote client. Args: aresponses: An aresponses server. authenticated_remote_client: A mock local controller. """ async with authenticated_remote_client, aiohttp.ClientSession() as session: client = Client(session=session) await client.load_remote(TEST_EMAIL, TEST_PASSWORD) assert len(client.controllers) == 1 controller = client.controllers[TEST_MAC] assert controller._access_token == TEST_ACCESS_TOKEN assert controller.api_version == TEST_API_VERSION assert controller.hardware_version == TEST_HW_VERSION assert controller.mac == TEST_MAC assert controller.name == TEST_NAME assert controller.software_version == TEST_SW_VERSION aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_load_remote_skip( aresponses: ResponsesMockServer, authenticated_remote_client: ResponsesMockServer, remote_auth_login_1_response: dict[str, Any], remote_sprinklers_response: dict[str, Any], ) -> None: """Test skipping the loading of a remote client if it's already loaded. Args: aresponses: An aresponses server. authenticated_remote_client: A mock local controller. remote_auth_login_1_response: An API response payload. remote_sprinklers_response: An API response payload. """ authenticated_remote_client.add( "my.rainmachine.com", "/login/auth", "post", response=aiohttp.web_response.json_response( remote_auth_login_1_response, status=200 ), ) authenticated_remote_client.add( "my.rainmachine.com", "/devices/get-sprinklers", "post", response=aiohttp.web_response.json_response( remote_sprinklers_response, status=200 ), ) async with authenticated_remote_client, aiohttp.ClientSession() as session: client = Client(session=session) await client.load_remote(TEST_EMAIL, TEST_PASSWORD, True) controller = client.controllers[TEST_MAC] await client.load_remote(TEST_EMAIL, TEST_PASSWORD, True) assert len(client.controllers) == 1 assert client.controllers[TEST_MAC] == controller aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio @pytest.mark.parametrize( "response_fixture_filename,exc", [ ("remote_error_http_body_response.json", RequestError), ("remote_error_known_response.json", RequestError), ("remote_error_unknown_response.json", RequestError), ("unauthenticated_response.json", TokenExpiredError), ], ) async def test_load_remote_errors( aresponses: ResponsesMockServer, exc: type[RainMachineError], response_fixture_filename: str, ) -> None: """Test various remote errors. Args: aresponses: An aresponses server. exc: A RainMachineError subclass. response_fixture_filename: A filename string. """ aresponses.add( "my.rainmachine.com", "/login/auth", "post", response=aiohttp.web_response.json_response( json.loads(load_fixture(response_fixture_filename)), status=200 ), ) with pytest.raises(exc): async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_remote(TEST_EMAIL, TEST_PASSWORD) aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_request_timeout( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test whether the client properly raises an error on timeout. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ with patch.object(aiohttp.ClientResponse, "json", side_effect=asyncio.TimeoutError): async with authenticated_local_client, aiohttp.ClientSession() as session: with pytest.raises(RequestError): client = Client(session=session, request_timeout=0) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_request_unknown_api_call( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test that an unknown API call is handled. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("unknown_api_call_response.json")), status=400 ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) with pytest.raises(UnknownAPICallError): _ = await controller.zones.all() aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_request_unparseable_response( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test a response that can't be parsed as JSON. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/zone", "get", aresponses.Response(text="404 Not Found", status=404), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) with pytest.raises(RequestError): _ = await controller.zones.all() aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_token_expired_explicit_exception( aresponses: ResponsesMockServer, ) -> None: """Test that the appropriate error is thrown when a token expires explicitly. Args: aresponses: An aresponses server. """ aresponses.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/auth/login", "post", response=aiohttp.web_response.json_response( json.loads(load_fixture("unauthenticated_response.json")), status=400 ), ) with pytest.raises(TokenExpiredError): async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local(TEST_HOST, TEST_PASSWORD, TEST_PORT, False) aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_token_expired_implicit_exception( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test that the appropriate error is thrown when a token expires implicitly. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ with pytest.raises(TokenExpiredError): async with authenticated_local_client, aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) controller._access_token_expiration = datetime.now() - timedelta(hours=1) await controller.request("get", "random/endpoint") aresponses.assert_plan_strictly_followed() @pytest.mark.asyncio async def test_retry_only_once_on_server_disconnected( aresponses: ResponsesMockServer, authenticated_local_client: ResponsesMockServer ) -> None: """Test we retry on server disconnected. Args: aresponses: An aresponses server. authenticated_local_client: A mock local controller. """ async with authenticated_local_client: authenticated_local_client.add( f"{TEST_HOST}:{TEST_PORT}", "/api/4/restrictions/raindelay", "get", response=aiohttp.web_response.json_response( json.loads(load_fixture("restrictions_raindelay_response.json")), status=400, ), ) async with aiohttp.ClientSession() as session: client = Client(session=session) await client.load_local( TEST_HOST, TEST_PASSWORD, port=TEST_PORT, use_ssl=False ) controller = next(iter(client.controllers.values())) patcher = None # pylint: disable=unused-argument def _raise_and_stop_patch( # type: ignore[no-untyped-def] *args, **kwargs ) -> None: nonlocal patcher if patcher: patcher.stop() # type: ignore[unreachable] raise aiohttp.ServerDisconnectedError patcher = patch.object( session, "request", side_effect=_raise_and_stop_patch ) patcher.start() data = await controller.restrictions.raindelay() assert data["delayCounter"] == -1 with pytest.raises(RequestError), patch.object( session, "request", side_effect=aiohttp.ServerDisconnectedError ): await controller.restrictions.raindelay() aresponses.assert_plan_strictly_followed()