pax_global_header00006660000000000000000000000064146372626460014532gustar00rootroot0000000000000052 comment=35c5828d53f8f0ce5cdd0bd075a0adb099d178e2 python-ring-doorbell-0.8.12/000077500000000000000000000000001463726264600157005ustar00rootroot00000000000000python-ring-doorbell-0.8.12/.github/000077500000000000000000000000001463726264600172405ustar00rootroot00000000000000python-ring-doorbell-0.8.12/.github/actions/000077500000000000000000000000001463726264600207005ustar00rootroot00000000000000python-ring-doorbell-0.8.12/.github/actions/setup/000077500000000000000000000000001463726264600220405ustar00rootroot00000000000000python-ring-doorbell-0.8.12/.github/actions/setup/action.yml000066400000000000000000000060471463726264600240470ustar00rootroot00000000000000--- name: Setup Environment description: Install requested pipx dependencies, configure the system python, and install poetry and the package dependencies inputs: poetry-install-options: default: "" poetry-version: default: 1.8.2 python-version: required: true cache-pre-commit: default: false runs: using: composite steps: - uses: "actions/setup-python@v5" id: setup-python with: python-version: "${{ inputs.python-version }}" - name: Setup pipx environment Variables id: pipx-env-setup # pipx default home and bin dir are not writable by the cache action # so override them here and add the bin dir to PATH for later steps. # This also ensures the pipx cache only contains poetry run: | SEP="${{ !startsWith(runner.os, 'windows') && '/' || '\\' }}" PIPX_CACHE="${{ github.workspace }}${SEP}pipx_cache" echo "pipx-cache-path=${PIPX_CACHE}" >> $GITHUB_OUTPUT echo "pipx-version=$(pipx --version)" >> $GITHUB_OUTPUT echo "PIPX_HOME=${PIPX_CACHE}${SEP}home" >> $GITHUB_ENV echo "PIPX_BIN_DIR=${PIPX_CACHE}${SEP}bin" >> $GITHUB_ENV echo "PIPX_MAN_DIR=${PIPX_CACHE}${SEP}man" >> $GITHUB_ENV echo "${PIPX_CACHE}${SEP}bin" >> $GITHUB_PATH shell: bash - name: Pipx cache id: pipx-cache uses: actions/cache@v4 with: path: ${{ steps.pipx-env-setup.outputs.pipx-cache-path }} key: ${{ runner.os }}-python-${{ steps.setup-python.outputs.python-version }}-pipx-${{ steps.pipx-env-setup.outputs.pipx-version }}-poetry-${{ inputs.poetry-version }} - name: Install poetry if: steps.pipx-cache.outputs.cache-hit != 'true' id: install-poetry shell: bash run: |- pipx install poetry==${{ inputs.poetry-version }} --python "${{ steps.setup-python.outputs.python-path }}" - name: Read poetry cache location id: poetry-cache-location shell: bash run: |- echo "poetry-venv-location=$(poetry config virtualenvs.path)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 name: Poetry cache with: path: | ${{ steps.poetry-cache-location.outputs.poetry-venv-location }} key: ${{ runner.os }}-python-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('poetry.lock') }}-options-${{ inputs.poetry-install-options }} - name: "Poetry install" shell: bash run: | poetry install ${{ inputs.poetry-install-options }} - name: Read pre-commit version if: inputs.cache-pre-commit == 'true' id: pre-commit-version shell: bash run: >- echo "pre-commit-version=$(poetry run pre-commit -V | awk '{print $2}')" >> $GITHUB_OUTPUT - uses: actions/cache@v4 if: inputs.cache-pre-commit == 'true' name: Pre-commit cache with: path: ~/.cache/pre-commit/ key: ${{ runner.os }}-pre-commit-${{ steps.pre-commit-version.outputs.pre-commit-version }}-python-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} python-ring-doorbell-0.8.12/.github/release-drafter.yml000066400000000000000000000000541463726264600230270ustar00rootroot00000000000000template: | ## What's Changed $CHANGES python-ring-doorbell-0.8.12/.github/workflows/000077500000000000000000000000001463726264600212755ustar00rootroot00000000000000python-ring-doorbell-0.8.12/.github/workflows/ci.yml000066400000000000000000000055611463726264600224220ustar00rootroot00000000000000name: CI on: push: branches: ["master"] pull_request: branches: ["master"] env: POETRY_VERSION: 1.8.2 jobs: linting: name: "Perform linting checks" runs-on: ubuntu-latest strategy: matrix: python-version: ["3.12"] steps: - uses: "actions/checkout@v4" - name: Setup environment uses: ./.github/actions/setup with: python-version: ${{ matrix.python-version }} poetry-version: ${{ env.POETRY_VERSION }} poetry-install-options: "--extras listen" cache-pre-commit: true - name: "Run pre-commit checks" run: | poetry run pre-commit run --all-files docs: name: "Build docs" needs: linting runs-on: ubuntu-latest strategy: matrix: python-version: ["3.12"] steps: - uses: "actions/checkout@v4" - name: Setup environment uses: ./.github/actions/setup with: python-version: ${{ matrix.python-version }} poetry-version: ${{ env.POETRY_VERSION }} poetry-install-options: "--extras docs --without dev" - name: Make docs poetry run: | poetry run make -C docs html tests: name: Tests - Python ${{ matrix.python-version}} on ${{ matrix.os }}${{ fromJSON('[" (listen)", ""]')[matrix.extras == ''] }} needs: linting runs-on: ubuntu-latest strategy: fail-fast: true matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.9", "pypy-3.10"] os: [ubuntu-latest, macos-latest, windows-latest] extras: [false, true] exclude: - os: macos-latest extras: true - os: windows-latest extras: true - os: ubuntu-latest python-version: "pypy-3.9" extras: true - os: ubuntu-latest python-version: "pypy-3.10" extras: true - os: ubuntu-latest python-version: "3.8" extras: true - os: ubuntu-latest python-version: "3.9" extras: true - os: ubuntu-latest python-version: "3.10" extras: true steps: - uses: "actions/checkout@v4" - name: Setup environment uses: ./.github/actions/setup with: python-version: ${{ matrix.python-version }} poetry-version: ${{ env.POETRY_VERSION }} poetry-install-options: ${{ matrix.extras == true && '--extras listen' || '' }} - name: Run tests run: > poetry run pytest tests/ --cov=ring_doorbell --cov-report=xml --cov-report=term-missing --import-mode importlib - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2.2.3 with: file: coverage.xml debug: true if: ${{ success() && matrix.python-version == '3.12' }} python-ring-doorbell-0.8.12/.github/workflows/locks-threads.yml000066400000000000000000000006441463726264600245670ustar00rootroot00000000000000name: Lock # yamllint disable-line rule:truthy on: schedule: - cron: "0 * * * *" jobs: lock: if: github.repository_owner == 'tchellomello' 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: "7" pr-lock-reason: "" python-ring-doorbell-0.8.12/.github/workflows/pythonpublish.yml000066400000000000000000000015421463726264600247320ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v1 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip python3 -m pip install --user pipx python3 -m pipx ensurepath pipx install poetry==1.6.1 - name: Build and publish run: | poetry build poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }} python-ring-doorbell-0.8.12/.github/workflows/release-drafter.yml000066400000000000000000000005101463726264600250610ustar00rootroot00000000000000name: Release Drafter on: push: branches: - master jobs: update_release_draft: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "master" - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} python-ring-doorbell-0.8.12/.github/workflows/stale.yml000066400000000000000000000052571463726264600231410ustar00rootroot00000000000000name: Stale # yamllint disable-line rule:truthy on: schedule: - cron: "0 * * * *" workflow_dispatch: jobs: stale: if: github.repository_owner == 'tchellomello' runs-on: ubuntu-latest steps: - name: Stale issues and prs uses: actions/stale@v9.0.0 with: repo-token: ${{ github.token }} days-before-stale: 90 days-before-close: 7 operations-per-run: 250 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no-stale,help-wanted,needs-more-information,waiting-for-reporter" stale-pr-label: "stale" exempt-pr-labels: "no-stale" stale-pr-message: > There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. If you are the author of this PR, please leave a comment if you want to keep it open. Also, please rebase your PR onto the latest dev branch to ensure that it's up to date with the latest changes. Thank you for your contribution! stale-issue-message: > There hasn't been any activity on this issue recently. This issue has been automatically marked as stale because of that. It will be closed if no further activity occurs. Please make sure to update to the latest ring_doorbell version and check if that solves the issue. Thank you for your contributions. - name: Needs-more-information and waiting-for-reporter stale issues policy uses: actions/stale@v9.0.0 with: repo-token: ${{ github.token }} only-labels: "needs-more-information,waiting-for-reporter" days-before-stale: 21 days-before-close: 7 days-before-pr-stale: -1 days-before-pr-close: -1 operations-per-run: 250 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 and it has been waiting for the reporter to provide information or an update. This issue has been automatically marked as stale because of that. It will be closed if no further activity occurs. Please make sure to update to the latest ring_doorbell version and check if that solves the issue. Thank you for your contributions. python-ring-doorbell-0.8.12/.gitignore000066400000000000000000000014341463726264600176720ustar00rootroot00000000000000# ---> Python # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Visual Studio .vs # Distribution / packaging .Python env/ venv/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ *.cache credentials.json *.code-workspacepython-ring-doorbell-0.8.12/.hound.yml000066400000000000000000000000311463726264600176100ustar00rootroot00000000000000python: enabled: false python-ring-doorbell-0.8.12/.pre-commit-config.yaml000066400000000000000000000011751463726264600221650ustar00rootroot00000000000000repos: - repo: https://github.com/python-poetry/poetry rev: 1.8.0 hooks: - id: poetry-check - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.3.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.9.0 hooks: - id: mypy additional_dependencies: [types-click, types-pytz, types-requests-oauthlib==1.3] args: ["--install-types", "--non-interactive", "--ignore-missing-imports"] exclude: | (?x)^( scripts/.*| docs/.*| tests/.*| test\.py )$ python-ring-doorbell-0.8.12/.readthedocs.yaml000066400000000000000000000021741463726264600211330ustar00rootroot00000000000000# Read the Docs configuration file for Sphinx projects # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # You can also specify other tool versions: # nodejs: "20" # rust: "1.70" # golang: "1.20" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/source/conf.py # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs # builder: "dirhtml" # Fail on all warnings to avoid broken references # fail_on_warning: true # Optionally build your docs in additional formats such as PDF and ePub # formats: # - pdf # - epub formats: all # Optional but recommended, declare the Python requirements required # to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html # python: # install: # - requirements: docs/requirements.txt python: install: - method: pip path: . extra_requirements: - docspython-ring-doorbell-0.8.12/CHANGELOG.rst000066400000000000000000000002401463726264600177150ustar00rootroot00000000000000========= Changelog ========= An up to date dynamic changelog can found on `readthedocs `_. python-ring-doorbell-0.8.12/CONTRIBUTING.rst000066400000000000000000000111631463726264600203430ustar00rootroot00000000000000============ Contributing ============ Contributions are welcome and very appreciated!! Keep in mind that every little contribution helps, don't matter what. Types of Contributions ---------------------- Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/tchellomello/python-ring-doorbell/issues If you are reporting a bug, please include: * Ring product and firmware version * Steps to reproduce the issue * Anything you judge interesting for the troubleshooting Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" and "help wanted" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. Documentation ~~~~~~~~~~~~~ Documentation is always good. So please feel free to add any documentation you think will help our users. Request Features ~~~~~~~~~~~~~~~~ File an issue at https://github.com/tchellomello/python-ring-doorbell/issues. Get Started! ------------ Ready to contribute? Here's how to set up `python-ring-doorbell` for local development. 1. Fork the `python-ring-doorbell` repo on GitHub. #. Clone your fork locally:: $ cd YOURDIRECTORYFORTHECODE $ git clone git@github.com:YOUR_GITHUB_USERNAME/python-ring-doorbell.git #. We are using `poetry `_ for dependency management. If you dont have poetry installed you can install it with:: $ curl -sSL https://install.python-poetry.org | python3 - This installs Poetry in a virtual environment to isolate it from the rest of your system. Then to install `python-ring-doorbell`:: $ poetry install Poetry will create a virtual environment for you and install all the requirements If you want to be able to build the docs (not necessary unless you are working on the doc generation):: $ poetry install --extras docs #. Create a branch for local development:: $ git checkout -b NAME-OF-YOUR-BUGFIX-OR-FEATURE Now you can make your changes locally. #. We are using `tox `_ for testing and linting:: $ poetry run tox -r #. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin NAME-OF-YOUR-BUGFIX-OR-FEATURE #. Submit a pull request through the GitHub website. Thank you!! Additional Notes ---------------- Poetry ~~~~~~ Dependencies ^^^^^^^^^^^^ Poetry is very useful at managing virtual environments and ensuring that dependencies all match up for you. It manages this with the use of the `poetry.lock` file which contains all the exact versions to be installed. This means that if you add any dependecies you should do it via:: $ poetry add pypi_project_name rather than pip. This will update `pyproject.toml` and `poetry.lock` accordingly. If you install something in the virtual environment directly via pip you will need to run:: $ poetry lock --no-update to resync the lock file but without updating all the other requirements to latest versions. To uninstall a dependency:: $ poetry remove pypi_project_name finally if you want to add a dependency for development only:: $ poetry add --group dev pypi_project_name Environments ^^^^^^^^^^^^ Poetry creates a virtual environment for the project and you can activate the virtual environment with:: $ poetry shell To exit the shell type ``exit`` rather than deactivate. However you don't **need** to activate the virtual environment and you can run any command without activating it by:: $ poetry run SOME_COMMAND It is possible to manage all this from within a virtual environment you create yourself but that requires installing poetry into the same virtual environment and this can potentially cause poetry to uninstall some of its own dependencies in certain situations. Hence the recommendation to install poetry into a seperate virtual environment of its via the install script above or pipx. See `poetry documentation `_ for more info Documentation ^^^^^^^^^^^^^ To build the docs install with the docs extra:: $ poetry install --extras docs Then generate a `Github access token `_ (no permissions are needed) and export it as follows:: $ export CHANGELOG_GITHUB_TOKEN="«your-40-digit-github-token»" Then build:: $ make -C html You can add the token to your shell profile to avoid having to export it each time. (e.g., .env, ~/.bash_profile, ~/.bashrc, ~/.zshrc, etc)python-ring-doorbell-0.8.12/LICENSE000066400000000000000000000167441463726264600167210ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. python-ring-doorbell-0.8.12/README.rst000066400000000000000000000212561463726264600173750ustar00rootroot00000000000000===================== Python Ring Door Bell ===================== .. image:: https://badge.fury.io/py/ring-doorbell.svg :alt: PyPI Version :target: https://badge.fury.io/py/ring-doorbell .. image:: https://github.com/tchellomello/python-ring-doorbell/actions/workflows/ci.yml/badge.svg?branch=master :alt: Build Status :target: https://github.com/tchellomello/python-ring-doorbell/actions/workflows/ci.yml?branch=master .. image:: https://coveralls.io/repos/github/tchellomello/python-ring-doorbell/badge.svg?branch=master :alt: Coverage :target: https://coveralls.io/github/tchellomello/python-ring-doorbell?branch=master .. image:: https://readthedocs.org/projects/python-ring-doorbell/badge/?version=latest :alt: Documentation Status :target: https://python-ring-doorbell.readthedocs.io/?badge=latest .. image:: https://img.shields.io/pypi/pyversions/ring-doorbell.svg :alt: Py Versions :target: https://pypi.python.org/pypi/ring-doorbell Python Ring Door Bell is a library written for Python 3.8+ that exposes the Ring.com devices as Python objects. There is also a command line interface that is work in progress. `Contributors welcome `_. *Currently Ring.com does not provide an official API. The results of this project are merely from reverse engineering.* Documentation: `http://python-ring-doorbell.readthedocs.io/ `_ Installation ------------ .. code-block:: bash # Installing from PyPi $ pip install ring_doorbell # Installing latest development $ pip install \ git+https://github.com/tchellomello/python-ring-doorbell@master Event Listener ++++++++++++++ If you want the ring api to listen for push events from ring.com for dings and motion you will need to install with the `listen` extra:: $ pip install ring_doorbell[listen] The api will then start listening for push events after you have first called `update_dings()` or `update_data()` but only if there is a running `asyncio `_ event loop (which there will be if using the CLI) Using the CLI ------------- The CLI is work in progress and currently has the following commands: 1. Show your devices:: $ ring-doorbell Or:: $ ring-doorbell show #. List your device names (with device kind):: $ ring-doorbell list #. Either count or download your vidoes or both:: $ ring-doorbell videos --count --download-all #. Enable disable motion detection:: $ ring-doorbell motion-detection --device-name "DEVICENAME" --on $ ring-doorbell motion-detection --device-name "DEVICENAME" --off #. Listen for push notifications like the ones sent to your phone:: $ ring-doorbell listen #. List your ring groups:: $ ring-doorbell groups #. Show your ding history:: $ ring-doorbell history --device-name "Front Door" #. Show your currently active dings:: $ ring-doorbell dings #. Query a ring api url directly:: $ ring-doorbell raw-query --url /clients_api/dings/active #. Run ``ring-doorbell --help`` or ``ring-doorbell videos --help`` for full options Initializing your Ring object ----------------------------- .. code-block:: python import getpass import json from pathlib import Path from ring_doorbell import Auth, AuthenticationError, Requires2FAError, Ring user_agent = "YourProjectName-1.0" # Change this cache_file = Path(user_agent + ".token.cache") def token_updated(token): cache_file.write_text(json.dumps(token)) def otp_callback(): auth_code = input("2FA code: ") return auth_code def do_auth(): username = input("Username: ") password = getpass.getpass("Password: ") auth = Auth(user_agent, None, token_updated) try: auth.fetch_token(username, password) except Requires2FAError: auth.fetch_token(username, password, otp_callback()) return auth def main(): if cache_file.is_file(): # auth token is cached auth = Auth(user_agent, json.loads(cache_file.read_text()), token_updated) ring = Ring(auth) try: ring.create_session() # auth token still valid except AuthenticationError: # auth token has expired auth = do_auth() else: auth = do_auth() # Get new auth token ring = Ring(auth) ring.update_data() devices = ring.devices() print(devices) if __name__ == "__main__": main() Listing devices linked to your account -------------------------------------- .. code-block:: python # All devices devices = ring.devices() {'chimes': [], 'doorbots': []} # All doorbells doorbells = devices['doorbots'] [] # All chimes chimes = devices['chimes'] [] # All stickup cams stickup_cams = devices['stickup_cams'] [] Playing with the attributes and functions ----------------------------------------- .. code-block:: python devices = ring.devices() for dev in list(devices['stickup_cams'] + devices['chimes'] + devices['doorbots']): dev.update_health_data() print('Address: %s' % dev.address) print('Family: %s' % dev.family) print('ID: %s' % dev.id) print('Name: %s' % dev.name) print('Timezone: %s' % dev.timezone) print('Wifi Name: %s' % dev.wifi_name) print('Wifi RSSI: %s' % dev.wifi_signal_strength) # setting dev volume print('Volume: %s' % dev.volume) dev.volume = 5 print('Volume: %s' % dev.volume) # play dev test shound if dev.family == 'chimes': dev.test_sound(kind = 'ding') dev.test_sound(kind = 'motion') # turn on lights on floodlight cam if dev.family == 'stickup_cams' and dev.lights: dev.lights = 'on' Showing door bell events ------------------------ .. code-block:: python devices = ring.devices() for doorbell in devices['doorbots']: # listing the last 15 events of any kind for event in doorbell.history(limit=15): print('ID: %s' % event['id']) print('Kind: %s' % event['kind']) print('Answered: %s' % event['answered']) print('When: %s' % event['created_at']) print('--' * 50) # get a event list only the triggered by motion events = doorbell.history(kind='motion') Downloading the last video triggered by a ding or motion event -------------------------------------------------------------- .. code-block:: python devices = ring.devices() doorbell = devices['doorbots'][0] doorbell.recording_download( doorbell.history(limit=100, kind='ding')[0]['id'], filename='last_ding.mp4', override=True) Displaying the last video capture URL ------------------------------------- .. code-block:: python print(doorbell.recording_url(doorbell.last_recording_id)) 'https://ring-transcoded-videos.s3.amazonaws.com/99999999.mp4?X-Amz-Expires=3600&X-Amz-Date=20170313T232537Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=TOKEN_SECRET/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=secret' Controlling a Light Group ------------------------- .. code-block:: python groups = ring.groups() group = groups['the-group-you-want'] print(group.lights) # Prints True if lights are on, False if off # Turn on lights indefinitely group.lights = True # Turn off lights group.lights = False # Turn on lights for 30 seconds group.lights = (True, 30) How to contribute ----------------- See our `Contributing Page `_. Credits && Thanks ----------------- * This project was inspired and based on https://github.com/jeroenmoors/php-ring-api. Many thanks @jeroenmoors. * A guy named MadBagger at Prism19 for his initial research (http://www.prism19.com/doorbot/second-pass-and-comm-reversing/) * The creators of mitmproxy (https://mitmproxy.org/) great http and https traffic inspector * @mfussenegger for his post on mitmproxy and virtualbox https://zignar.net/2015/12/31/sniffing-vbox-traffic-mitmproxy/ * To the project http://www.android-x86.org/ which allowed me to install Android on KVM. * Many thanks to Carles Pina I Estany for creating the python-ring-doorbell Debian Package (https://tracker.debian.org/pkg/python-ring-doorbell). python-ring-doorbell-0.8.12/RELEASING.rst000066400000000000000000000015121463726264600177420ustar00rootroot00000000000000========= Releasing ========= These are the steps to create a release ======================================= 1. Create Release branch:: $ git checkout -b 0.7.x-release master #. Bump version number:: $ poetry version 0.7.x #. Create a draft release in GitHub Generate the release notes in GitHub Add to the top of the CHANGELOG.rst file (TODO automate this step into the docs build) #. Test the build:: $ poetry build Check the dist directory sources are as expected #. Commit changes and merge to master:: $ git commit -m "Bump version to 0.7.x" Create and merge PR into master #. Publish the release on GitHub GitHub workflow `pythonpublish.yml` will then publish to pypi.org readthedocs will compile the latest docs #. Check the release Verify on pypi and readthedocs.iopython-ring-doorbell-0.8.12/_config.yml000066400000000000000000000000331463726264600200230ustar00rootroot00000000000000theme: jekyll-theme-minimalpython-ring-doorbell-0.8.12/docs/000077500000000000000000000000001463726264600166305ustar00rootroot00000000000000python-ring-doorbell-0.8.12/docs/Makefile000066400000000000000000000011761463726264600202750ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) python-ring-doorbell-0.8.12/docs/make.bat000066400000000000000000000014441463726264600202400ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source set BUILDDIR=build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org/ exit /b 1 ) if "%1" == "" goto help %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd python-ring-doorbell-0.8.12/docs/ruff.toml000066400000000000000000000002131463726264600204630ustar00rootroot00000000000000# This extends our general Ruff rules specifically for docs extend = "../pyproject.toml" lint.extend-ignore = [ "D100", "D103", ] python-ring-doorbell-0.8.12/docs/source/000077500000000000000000000000001463726264600201305ustar00rootroot00000000000000python-ring-doorbell-0.8.12/docs/source/changelog.rst000066400000000000000000000004001463726264600226030ustar00rootroot00000000000000========= Changelog ========= .. changelog:: :changelog-url: https://python-ring-doorbell.readthedocs.io/latest/changelog.html :github: https://github.com/tchellomello/python-ring-doorbell/releases/ :pypi: https://pypi.org/project/ring-doorbell/ python-ring-doorbell-0.8.12/docs/source/conf.py000066400000000000000000000025561463726264600214370ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # noqa: INP001 # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html from __future__ import annotations import os from importlib.metadata import version as _version # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "python-ring-doorbell" copyright = "2023, Marcelo Moreira de Mello" # noqa: A001 author = "Marcelo Moreira de Mello" release = _version("ring_doorbell") version = _version("ring_doorbell") # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ "sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.viewcode", "sphinx.ext.todo", "sphinx_github_changelog", ] templates_path = ["_templates"] exclude_patterns: list[str] = [] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] master_doc = "index" sphinx_github_changelog_token = os.environ.get("CHANGELOG_GITHUB_TOKEN") python-ring-doorbell-0.8.12/docs/source/contributing.rst000066400000000000000000000000431463726264600233660ustar00rootroot00000000000000.. include:: ../../CONTRIBUTING.rstpython-ring-doorbell-0.8.12/docs/source/index.rst000066400000000000000000000007311463726264600217720ustar00rootroot00000000000000.. python-ring-doorbell documentation master file, created by sphinx-quickstart on Fri Sep 22 17:28:31 2023. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to python-ring-doorbell's documentation! ================================================ .. include:: ../../README.rst .. toctree:: :hidden: :titlesonly: :maxdepth: 0 Home contributing changelog python-ring-doorbell-0.8.12/poetry.lock000066400000000000000000002677361463726264600201210ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" optional = true python-versions = ">=3.6" files = [ {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, ] [[package]] name = "anyio" version = "4.3.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] [[package]] name = "asyncclick" version = "8.1.7.2" description = "Composable command line interface toolkit, async version" optional = false python-versions = ">=3.7" files = [ {file = "asyncclick-8.1.7.2-py3-none-any.whl", hash = "sha256:1ab940b04b22cb89b5b400725132b069d01b0c3472a9702c7a2c9d5d007ded02"}, {file = "asyncclick-8.1.7.2.tar.gz", hash = "sha256:219ea0f29ccdc1bb4ff43bcab7ce0769ac6d48a04f997b43ec6bee99a222daa0"}, ] [package.dependencies] anyio = "*" colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "babel" version = "2.14.0" description = "Internationalization utilities" optional = true python-versions = ">=3.7" files = [ {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] [package.dependencies] pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} [package.extras] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[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 = "cffi" version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = true python-versions = ">=3.8" files = [ {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] [package.dependencies] pycparser = "*" [[package]] name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "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.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "cryptography" version = "42.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = true python-versions = ">=3.7" files = [ {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"}, {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"}, {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"}, {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"}, {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"}, {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"}, {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"}, {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"}, {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"}, {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"}, {file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"}, {file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"}, {file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"}, {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"}, {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"}, {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"}, {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"}, {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"}, {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"}, {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"}, {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"}, {file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"}, {file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"}, {file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"}, {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"}, {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"}, {file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"}, {file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"}, {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"}, {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"}, {file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"}, {file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] nox = ["nox"] pep8test = ["check-sdist", "click", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] name = "distlib" version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] name = "docutils" version = "0.18.1" description = "Docutils -- Python Documentation Utilities" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, ] [[package]] name = "exceptiongroup" version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" version = "3.13.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ {file = "filelock-3.13.4-py3-none-any.whl", hash = "sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f"}, {file = "filelock-3.13.4.tar.gz", hash = "sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] typing = ["typing-extensions (>=4.8)"] [[package]] name = "firebase-messaging" version = "0.2.1" description = "FCM/GCM push notification client" optional = true python-versions = ">=3.8,<4.0" files = [ {file = "firebase_messaging-0.2.1-py3-none-any.whl", hash = "sha256:46961137d131ab9d1603508e1111438a1ed4fb97093be7dbfbe789bc8424dd7c"}, {file = "firebase_messaging-0.2.1.tar.gz", hash = "sha256:16d754c16edfd872eceae8ba85b8989fab811f4ff2654f9fcfd988ea565c326d"}, ] [package.dependencies] cryptography = ">=2.5" http-ece = ">=1.1.0,<2.0.0" protobuf = ">=4.24.3,<5.0.0" requests = ">=2.19.0" [package.extras] docs = ["sphinx (==7.1.2)", "sphinx-autodoc-typehints (>=1.24.0,<2.0.0)", "sphinx-rtd-theme (>=1.3.0,<2.0.0)"] [[package]] name = "http-ece" version = "1.2.0" description = "Encrypted Content Encoding for HTTP" optional = true python-versions = "*" files = [ {file = "http_ece-1.2.0.tar.gz", hash = "sha256:b5920f8efb8e1b5fb025713e3b36fda54336262010634b26dc1f98f85d1eb3de"}, ] [package.dependencies] cryptography = ">=2.5" [[package]] name = "identify" version = "2.5.35" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] [[package]] name = "importlib-metadata" version = "7.1.0" description = "Read metadata from Python packages" optional = true python-versions = ">=3.8" files = [ {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "jinja2" version = "3.1.3" description = "A very fast and expressive template engine." optional = true python-versions = ">=3.7" files = [ {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "markupsafe" version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = true python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] name = "mock" version = "5.1.0" description = "Rolling backport of unittest.mock for all Pythons" optional = false python-versions = ">=3.6" files = [ {file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"}, {file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"}, ] [package.extras] build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest", "pytest-cov"] [[package]] name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, ] [package.dependencies] setuptools = "*" [[package]] name = "oauthlib" version = "3.2.2" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false python-versions = ">=3.6" files = [ {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, ] [package.extras] rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "packaging" version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] name = "platformdirs" version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" files = [ {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[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.5.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.8" files = [ {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "protobuf" version = "4.25.3" description = "" optional = true python-versions = ">=3.8" files = [ {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, ] [[package]] name = "pycparser" version = "2.22" description = "C parser in Python" optional = true python-versions = ">=3.8" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] [[package]] name = "pygments" version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = true 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 = "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-asyncio" version = "0.23.6" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, ] [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 = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" files = [ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-mock" version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" files = [ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, ] [package.dependencies] pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-socket" version = "0.6.0" description = "Pytest Plugin to disable socket calls during tests" optional = false python-versions = ">=3.7,<4.0" files = [ {file = "pytest_socket-0.6.0-py3-none-any.whl", hash = "sha256:cca72f134ff01e0023c402e78d31b32e68da3efdf3493bf7788f8eba86a6824c"}, {file = "pytest_socket-0.6.0.tar.gz", hash = "sha256:363c1d67228315d4fc7912f1aabfd570de29d0e3db6217d61db5728adacd7138"}, ] [package.dependencies] pytest = ">=3.6.3" [[package]] name = "pytz" version = "2024.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [[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_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 = "requests" version = "2.31.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-mock" version = "1.12.1" description = "Mock out responses from the requests package" optional = false python-versions = ">=3.5" files = [ {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, ] [package.dependencies] requests = ">=2.22,<3" [package.extras] fixture = ["fixtures"] [[package]] name = "requests-oauthlib" version = "1.3.1" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, ] [package.dependencies] oauthlib = ">=3.0.0" requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "ruff" version = "0.3.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, ] [[package]] name = "setuptools" version = "69.5.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "sniffio" version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = true python-versions = "*" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] [[package]] name = "sphinx" version = "7.1.2" description = "Python documentation generator" optional = true python-versions = ">=3.8" files = [ {file = "sphinx-7.1.2-py3-none-any.whl", hash = "sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe"}, {file = "sphinx-7.1.2.tar.gz", hash = "sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f"}, ] [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.18.1,<0.21" imagesize = ">=1.3" importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.13" requests = ">=2.25.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-simplify", "isort", "mypy (>=0.990)", "ruff", "sphinx-lint", "types-requests"] test = ["cython", "filelock", "html5lib", "pytest (>=4.6)"] [[package]] name = "sphinx-github-changelog" version = "1.3.0" description = "Build a sphinx changelog from GitHub Releases" optional = true python-versions = ">=3.8,<4.0" files = [ {file = "sphinx_github_changelog-1.3.0-py3-none-any.whl", hash = "sha256:eb5424d590ae7866e77b8db7eecf283678cba76b74d90b17bc4f3872976407eb"}, {file = "sphinx_github_changelog-1.3.0.tar.gz", hash = "sha256:b898adc52131147305b9cb893c2a4cad0ba2912178ed8f88b62bf6f43a2baaa4"}, ] [package.dependencies] docutils = "*" requests = "*" Sphinx = "*" [[package]] name = "sphinx-rtd-theme" version = "1.3.0" description = "Read the Docs theme for Sphinx" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ {file = "sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0"}, {file = "sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931"}, ] [package.dependencies] docutils = "<0.19" sphinx = ">=1.6,<8" sphinxcontrib-jquery = ">=4,<5" [package.extras] dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] [[package]] name = "sphinxcontrib-applehelp" version = "1.0.4" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = true python-versions = ">=3.8" files = [ {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." optional = true python-versions = ">=3.5" files = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = true python-versions = ">=3.8" files = [ {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jquery" version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = true python-versions = ">=2.7" files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, ] [package.dependencies] Sphinx = ">=1.8" [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = true python-versions = ">=3.5" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] [package.extras] test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." optional = true python-versions = ">=3.5" files = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." optional = true python-versions = ">=3.5" files = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[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 = "types-click" version = "7.1.8" description = "Typing stubs for click" optional = false python-versions = "*" files = [ {file = "types-click-7.1.8.tar.gz", hash = "sha256:b6604968be6401dc516311ca50708a0a28baa7a0cb840efd7412f0dbbff4e092"}, {file = "types_click-7.1.8-py3-none-any.whl", hash = "sha256:8cb030a669e2e927461be9827375f83c16b8178c365852c060a34e24871e7e81"}, ] [[package]] name = "types-oauthlib" version = "3.2.0.20240217" description = "Typing stubs for oauthlib" optional = false python-versions = ">=3.8" files = [ {file = "types-oauthlib-3.2.0.20240217.tar.gz", hash = "sha256:2b58e68b549f37ea1fe725cd7e78661a58679c16d2e83d53f1e531588ad72223"}, {file = "types_oauthlib-3.2.0.20240217-py3-none-any.whl", hash = "sha256:aa8b50a6737a0c75ea2a19e4598cfabd767ca5fba8d5310dc736afaa179db14d"}, ] [[package]] name = "types-pytz" version = "2024.1.0.20240417" description = "Typing stubs for pytz" optional = false python-versions = ">=3.8" files = [ {file = "types-pytz-2024.1.0.20240417.tar.gz", hash = "sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981"}, {file = "types_pytz-2024.1.0.20240417-py3-none-any.whl", hash = "sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659"}, ] [[package]] name = "types-requests" version = "2.31.0.20240406" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ {file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"}, {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, ] [package.dependencies] urllib3 = ">=2" [[package]] name = "types-requests-oauthlib" version = "1.3.0.0" description = "Typing stubs for requests-oauthlib" optional = false python-versions = "*" files = [ {file = "types-requests-oauthlib-1.3.0.0.tar.gz", hash = "sha256:66f3138dac647dd33012c31477da1c98229d858cf9c2ef932ad2807656a32895"}, {file = "types_requests_oauthlib-1.3.0.0-py3-none-any.whl", hash = "sha256:e858d9189eed76eb236ec7fdee64621aa87f8d23d2d52727350fd93a85da8662"}, ] [package.dependencies] types-oauthlib = "*" types-requests = "*" [[package]] name = "typing-extensions" version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] [[package]] name = "urllib3" version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" version = "20.25.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ {file = "virtualenv-20.25.3-py3-none-any.whl", hash = "sha256:8aac4332f2ea6ef519c648d0bc48a5b1d324994753519919bddbb1aff25a104e"}, {file = "virtualenv-20.25.3.tar.gz", hash = "sha256:7bb554bbdfeaacc3349fa614ea5bff6ac300fc7c335e9facf3a3bcfc703f45be"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "zipp" version = "3.18.1" description = "Backport of pathlib-compatible object wrapper for zip files" optional = true python-versions = ">=3.8" files = [ {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [extras] docs = ["sphinx", "sphinx-github-changelog", "sphinx-rtd-theme"] listen = ["firebase-messaging"] [metadata] lock-version = "2.0" python-versions = "^3.8" content-hash = "928822147fee70007783115c908e4bad006a823b90613435927309bde911f09a" python-ring-doorbell-0.8.12/pyproject.toml000066400000000000000000000064221463726264600206200ustar00rootroot00000000000000[tool.poetry] name = "ring-doorbell" version = "0.8.12" description = "A Python library to communicate with Ring Door Bell (https://ring.com/)" authors = ["Marcelo Moreira de Mello "] license = "LGPL-3.0-or-later" readme = "README.rst" homepage = "https://github.com/tchellomello/python-ring-doorbell" repository = "https://github.com/tchellomello/python-ring-doorbell" documentation = "http://python-ring-doorbell.readthedocs.io/" keywords = [ "ring", "door bell", "camera", "home automation", ] classifiers = [ "Environment :: Other Environment", "Intended Audience :: Developers", "Operating System :: OS Independent", "Topic :: Home Automation", "Topic :: Software Development :: Libraries :: Python Modules" ] packages = [ { include = "ring_doorbell" }, { include = "tests", format = "sdist" }, ] include = [ { path = "CHANGELOG.rst", format = "sdist" } ] [tool.poetry.urls] "Bug Tracker" = "https://github.com/tchellomello/python-ring-doorbell/issues" [tool.poetry.scripts] ring-doorbell = "ring_doorbell.cli:cli" [tool.poetry.dependencies] python = "^3.8" requests = ">=2.0.0" requests-oauthlib = ">=1.3.0,<2" oauthlib = ">=3.0.0,<4" pytz = ">=2022.0" asyncclick = ">=8" anyio = "*" # see https://github.com/python-trio/asyncclick/issues/18 sphinx = {version = "<7.2.6", optional = true} sphinx-rtd-theme = {version = "^1.3.0", optional = true} sphinx-github-changelog = {version = "^1.2.1", optional = true} firebase-messaging = {version = "^0.2.1", optional = true} [tool.poetry.group.dev.dependencies] mock = "*" pre-commit = "*" pytest = "*" pytest-cov = "*" requests-mock = "*" pytest-asyncio = "*" pytest-mock = "*" pytest-socket = "^0.6.0" ruff = "*" types-requests-oauthlib = "1.3" types-pytz = "^2024.1.0.20240203" types-click = "^7.1.8" [tool.poetry.extras] docs = ["sphinx", "sphinx-rtd-theme", "sphinx-github-changelog"] listen = ["firebase-messaging"] [tool.pytest.ini_options] testpaths = [ "tests" ] norecursedirs = ".git" asyncio_mode = "auto" addopts = "--allow-hosts=127.0.0.1,::1" filterwarnings = [ "ignore:.*google._upb._message.MessageMapContainer uses PyType_Spec.*:DeprecationWarning", "ignore:.*google._upb._message.ScalarMapContainer uses PyType_Spec.*:DeprecationWarning", "ignore:.*datetime.datetime.utcnow.*:DeprecationWarning" ] [tool.coverage.run] source = ["ring_doorbell"] branch = true [tool.ruff] target-version = "py38" [tool.ruff.lint] ignore = [ "ANN101", # Self... explanatory "ANN102", # cls... just as useless "ANN401", # Opinioated warning on disallowing dynamically typed expressions "COM812", # Conflicts with other rules "D203", # Conflicts with other rules "D213", # Conflicts with other rules "D417", # False positives in some occasions "ISC001", # Conflicts with other rules "PLR2004", # Just annoying, not really useful ] select = ["ALL"] exclude = [ "ring_doorbell/cli.py", "test.py" ] [tool.ruff.lint.pydocstyle] convention = "pep257" [tool.mypy] exclude = [ '/scripts/', # TOML literal string (single-quotes, no escaping necessary) 'tests/.*', ] disallow_untyped_defs = true [[tool.mypy.overrides]] module = [ "ring_doorbell.cli" ] disallow_untyped_defs = false [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" python-ring-doorbell-0.8.12/ring_doorbell/000077500000000000000000000000001463726264600205215ustar00rootroot00000000000000python-ring-doorbell-0.8.12/ring_doorbell/__init__.py000066400000000000000000000020461463726264600226340ustar00rootroot00000000000000"""Python Package for interacting with Ring devices.""" from importlib.metadata import version __version__ = version("ring_doorbell") from ring_doorbell.auth import Auth from ring_doorbell.chime import RingChime from ring_doorbell.const import RingCapability, RingEventKind from ring_doorbell.doorbot import RingDoorBell from ring_doorbell.event import RingEvent from ring_doorbell.exceptions import ( AuthenticationError, Requires2FAError, RingError, RingTimeout, ) from ring_doorbell.generic import RingGeneric from ring_doorbell.group import RingLightGroup from ring_doorbell.other import RingOther from ring_doorbell.ring import Ring, RingDevices from ring_doorbell.stickup_cam import RingStickUpCam __all__ = [ "Ring", "Auth", "RingDevices", "RingChime", "RingCapability", "RingEventKind", "RingStickUpCam", "RingLightGroup", "RingDoorBell", "RingOther", "RingEvent", "RingError", "AuthenticationError", "Requires2FAError", "RingTimeout", "RingGeneric", "RingEvent", ] python-ring-doorbell-0.8.12/ring_doorbell/auth.py000066400000000000000000000126051463726264600220400ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Auth Class.""" from __future__ import annotations import uuid from typing import Any, Callable from oauthlib.oauth2 import ( LegacyApplicationClient, MissingTokenError, OAuth2Error, TokenExpiredError, ) from requests import HTTPError, Response, Timeout from requests.adapters import HTTPAdapter, Retry from requests_oauthlib import OAuth2Session from ring_doorbell.const import API_URI, NAMESPACE_UUID, TIMEOUT, OAuth from ring_doorbell.exceptions import ( AuthenticationError, Requires2FAError, RingError, RingTimeout, ) class Auth: """A Python Auth class for Ring.""" def __init__( self, user_agent: str, token: dict[str, Any] | None = None, token_updater: Callable[[dict[str, Any]], None] | None = None, hardware_id: str | None = None, ) -> None: """Initialise the auth class. :type token: Optional[Dict[str, str]] :type token_updater: Optional[Callable[[str], None]] """ self.user_agent = user_agent if hardware_id: self.hardware_id = hardware_id else: # Generate a UUID that will stay the same # for this physical device to prevent # multiple auth entries in ring.com self.hardware_id = str( uuid.uuid5(uuid.UUID(NAMESPACE_UUID), str(uuid.getnode()) + user_agent) ) self.device_model = "ring-doorbell:" + user_agent self.token_updater = token_updater self._oauth = OAuth2Session( client=LegacyApplicationClient(client_id=OAuth.CLIENT_ID), token=token ) retries = Retry(connect=5, read=0, backoff_factor=2) self._oauth.mount(API_URI, HTTPAdapter(max_retries=retries)) def fetch_token( self, username: str, password: str, otp_code: str | None = None ) -> dict[str, Any]: """Fetch initial token with username/password & 2FA. :type username: str :type password: str :type otp_code: str. """ headers = {"User-Agent": self.user_agent, "hardware_id": self.hardware_id} if otp_code: headers["2fa-support"] = "true" headers["2fa-code"] = otp_code try: token = self._oauth.fetch_token( OAuth.ENDPOINT, username=username, password=password, scope=OAuth.SCOPE, headers=headers, ) except MissingTokenError as ex: raise Requires2FAError from ex except OAuth2Error as ex: raise AuthenticationError(ex) from ex if self.token_updater is not None: self.token_updater(token) return token def refresh_tokens(self) -> dict[str, Any]: """Refresh the auth tokens.""" try: token = self._oauth.refresh_token( OAuth.ENDPOINT, headers={"User-Agent": self.user_agent} ) except OAuth2Error as ex: raise AuthenticationError(ex) from ex if self.token_updater is not None: self.token_updater(token) return token def get_hardware_id(self) -> str: """Get hardware ID.""" return self.hardware_id def get_device_model(self) -> str: """Get device model.""" return self.device_model def query( # noqa: C901, PLR0913, PLR0912 self, url: str, method: str = "GET", extra_params: dict[str, Any] | None = None, data: bytes | None = None, json: dict[Any, Any] | None = None, timeout: float | None = None, *, raise_for_status: bool = True, ) -> Response: """Query data from Ring API.""" if timeout is None: timeout = TIMEOUT params = {} if extra_params: params.update(extra_params) kwargs: dict[str, Any] = { "params": params, "headers": {"User-Agent": self.user_agent}, "timeout": timeout, } if json is not None: kwargs["json"] = json kwargs["headers"]["Content-Type"] = "application/json" # type: ignore[index] if data is not None: kwargs["data"] = data try: try: resp = self._oauth.request(method, url, **kwargs) except TokenExpiredError: self._oauth.token = self.refresh_tokens() resp = self._oauth.request(method, url, **kwargs) except AuthenticationError: raise # refresh_tokens will return this error if not valid except Timeout as ex: msg = f"Timeout error during query of url {url}: {ex}" raise RingTimeout(msg) from ex except Exception as ex: # noqa: BLE001 msg = f"Unknown error during query of url {url}: {ex}" raise RingError(msg) from ex if resp.status_code == 401: # Check whether there's an issue with the token grant self._oauth.token = self.refresh_tokens() if raise_for_status: try: resp.raise_for_status() except HTTPError as ex: msg = ( f"HTTP error with status code {resp.status_code} " f"during query of url {url}: {ex}" ) raise RingError(msg) from ex return resp python-ring-doorbell-0.8.12/ring_doorbell/chime.py000066400000000000000000000056301463726264600221640ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Chime wrapper.""" from __future__ import annotations import logging from typing import Any from ring_doorbell.const import ( CHIME_KINDS, CHIME_PRO_KINDS, CHIME_TEST_SOUND_KINDS, CHIME_VOL_MAX, CHIME_VOL_MIN, CHIMES_ENDPOINT, HEALTH_CHIMES_ENDPOINT, LINKED_CHIMES_ENDPOINT, MSG_VOL_OUTBOUND, TESTSOUND_CHIME_ENDPOINT, RingCapability, RingEventKind, ) from ring_doorbell.exceptions import RingError from ring_doorbell.generic import RingGeneric _LOGGER = logging.getLogger(__name__) class RingChime(RingGeneric): """Implementation for Ring Chime.""" @property def family(self) -> str: """Return Ring device family type.""" return "chimes" def update_health_data(self) -> None: """Update health attrs.""" self._health_attrs = ( self._ring.query(HEALTH_CHIMES_ENDPOINT.format(self.device_api_id)) .json() .get("device_health", {}) ) @property def model(self) -> str: """Return Ring device model name.""" if self.kind in CHIME_KINDS: return "Chime" if self.kind in CHIME_PRO_KINDS: return "Chime Pro" return "Unknown Chime" def has_capability(self, capability: RingCapability | str) -> bool: """Return if device has specific capability.""" capability = ( capability if isinstance(capability, RingCapability) else RingCapability.from_name(capability) ) if capability == RingCapability.VOLUME: return True return False @property def volume(self) -> int: """Return if chime volume.""" return self._attrs["settings"].get("volume", 0) @volume.setter def volume(self, value: int) -> None: if not ((isinstance(value, int)) and (CHIME_VOL_MIN <= value <= CHIME_VOL_MAX)): raise RingError(MSG_VOL_OUTBOUND.format(CHIME_VOL_MIN, CHIME_VOL_MAX)) params = { "chime[description]": self.name, "chime[settings][volume]": str(value), } url = CHIMES_ENDPOINT.format(self.device_api_id) self._ring.query(url, extra_params=params, method="PUT") self._ring.update_devices() @property def linked_tree(self) -> dict[str, Any]: """Return doorbell data linked to chime.""" url = LINKED_CHIMES_ENDPOINT.format(self.device_api_id) return self._ring.query(url).json() def test_sound(self, kind: RingEventKind | str = RingEventKind.DING) -> bool: """Play chime to test sound.""" kind_str = kind.value if isinstance(kind, RingEventKind) else kind if kind_str not in CHIME_TEST_SOUND_KINDS: return False url = TESTSOUND_CHIME_ENDPOINT.format(self.device_api_id) self._ring.query(url, method="POST", extra_params={"kind": kind_str}) return True python-ring-doorbell-0.8.12/ring_doorbell/cli.py000066400000000000000000000476461463726264600216630ustar00rootroot00000000000000# vim:sw=4:ts=4:et # Many thanks to @troopermax """Python Ring command line interface.""" from __future__ import annotations import asyncio import functools import getpass import json import logging import select import sys import traceback from datetime import datetime from pathlib import Path, PurePath from typing import Sequence, cast import asyncclick as click from ring_doorbell import ( Auth, AuthenticationError, Requires2FAError, Ring, RingDoorBell, RingEvent, RingGeneric, ) from ring_doorbell.const import CLI_TOKEN_FILE, GCM_TOKEN_FILE, PACKAGE_NAME, USER_AGENT from ring_doorbell.listen import can_listen def _header() -> None: _bar() echo("Ring CLI") def _bar() -> None: echo("---------------------------------") click.anyio_backend = "asyncio" # type: ignore[attr-defined] pass_ring = click.make_pass_decorator(Ring) cache_file = Path(CLI_TOKEN_FILE) gcm_cache_file = Path(GCM_TOKEN_FILE) class ExceptionHandlerGroup(click.Group): """Group to capture all exceptions and echo them nicely. Idea from https://stackoverflow.com/a/44347763 """ def __call__(self, *args, **kwargs): """Run the coroutine in the event loop and echo any exceptions.""" try: asyncio.run(self.main(*args, **kwargs)) except asyncio.CancelledError: pass except KeyboardInterrupt: echo("Cli interrupted with keyboard interrupt") except Exception as ex: # pylint: disable=broad-exception-caught echo(f"Got error: {ex!r}") traceback.print_exc() class MutuallyExclusiveOption(click.Option): """Prevents incompatable options being supplied, i.e. on and off.""" def __init__(self, *args, **kwargs) -> None: self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", [])) _help = kwargs.get("help", "") if self.mutually_exclusive: ex_str = ", ".join(self.mutually_exclusive) kwargs["help"] = _help + ( " NOTE: This argument is mutually exclusive with " " arguments: [" + ex_str + "]." ) super().__init__(*args, **kwargs) async def handle_parse_result(self, ctx, opts, args): if self.mutually_exclusive.intersection(opts) and self.name in opts: msg = ( "Illegal usage: `{}` is mutually exclusive with " "arguments `{}`.".format(self.name, ", ".join(self.mutually_exclusive)) ) raise click.UsageError(msg) return await super().handle_parse_result(ctx, opts, args) echo = click.echo def token_updated(token) -> None: """Writes token to file.""" cache_file.write_text(json.dumps(token), encoding="utf-8") def _format_filename(device_name, event): if not isinstance(event, dict): return None answered_status = "answered" if event["answered"] else "not_answered" filename = "{}_{}_{}_{}_{}".format( device_name, event["created_at"], event["kind"], answered_status, event["id"] ) return filename.replace(" ", "_").replace(":", ".") + ".mp4" def _do_auth(username, password, user_agent=USER_AGENT): if not username: username = input("Username: ") if not password: password = getpass.getpass("Password: ") auth = Auth(user_agent, None, token_updated) try: auth.fetch_token(username, password) return auth except Requires2FAError: auth.fetch_token(username, password, input("2FA Code: ")) return auth def _get_ring(username, password, do_update_data, user_agent=USER_AGENT): # connect to Ring account global cache_file, gcm_cache_file if user_agent != USER_AGENT: cache_file = Path(user_agent + ".token.cache") gcm_cache_file = Path(user_agent + ".gcm_token.cache") if cache_file.is_file(): auth = Auth( user_agent, json.loads(cache_file.read_text(encoding="utf-8")), token_updated, ) ring = Ring(auth) do_method = ring.update_data if do_update_data else ring.create_session try: do_method() except AuthenticationError: auth = _do_auth(username, password) ring = Ring(auth) do_method = ring.update_data if do_update_data else ring.create_session do_method() else: auth = _do_auth(username, password, user_agent=user_agent) ring = Ring(auth) do_method = ring.update_data if do_update_data else ring.create_session do_method() return ring @click.group( invoke_without_command=True, cls=ExceptionHandlerGroup, ) @click.version_option(package_name="ring_doorbell") @click.option( "--username", default=None, required=False, envvar="RING_USERNAME", help="Username for Ring account.", ) @click.option( "--password", default=None, required=False, envvar="RING_PASSWORD", help="Password for Ring account", ) @click.option("-d", "--debug", default=False, is_flag=True) @click.option( "--user-agent", default=USER_AGENT, required=False, envvar="RING_USER_AGENT", help="User agent to send to ring", ) @click.pass_context async def cli(ctx, username, password, debug, user_agent): """Command line function.""" _header() logging.basicConfig() log_level = logging.DEBUG if debug else logging.INFO logger = logging.getLogger(PACKAGE_NAME) logger.setLevel(log_level) if can_listen: logger = logging.getLogger("firebase_messaging") logger.setLevel(log_level) no_update_commands = ["listen"] no_update = ctx.invoked_subcommand in no_update_commands ring = _get_ring(username, password, not no_update, user_agent) ctx.obj = ring if ctx.invoked_subcommand is None: return await ctx.invoke(show) return None @cli.command(name="list") @pass_ring async def list_command(ring: Ring) -> None: """List ring devices.""" devices = ring.devices() device: RingGeneric | None = None for device in devices.doorbots: echo(device) for device in devices.authorized_doorbots: echo(device) for device in devices.chimes: echo(device) for device in devices.stickup_cams: echo(device) for device in devices.other: echo(device) @cli.command() @pass_ring @click.pass_context @click.option( "--on", "turn_on", cls=MutuallyExclusiveOption, is_flag=True, default=None, required=False, mutually_exclusive=["--off"], ) @click.option( "--off", "turn_off", cls=MutuallyExclusiveOption, is_flag=True, default=None, required=False, mutually_exclusive=["--on"], ) @click.option( "--device-name", "-dn", required=True, default=None, help="Name of the ring device", ) async def motion_detection(ctx, ring: Ring, device_name, turn_on, turn_off): """Display ring devices.""" device = ring.get_device_by_name(device_name) if not device: echo( f"No device with name {device_name} found." + " List of found device names (kind) is:" ) return await ctx.invoke(list_command) if not device.has_capability("motion_detection"): echo(f"{device!s} is not capable of motion detection") return None device = cast(RingDoorBell, device) state = "on" if device.motion_detection else "off" if not turn_on and not turn_off: echo(f"{device!s} has motion detection {state}") return None is_on = device.motion_detection if (turn_on and is_on) or (turn_off and not is_on): echo(f"{device!s} already has motion detection {state}") return None device.motion_detection = turn_on if turn_on else False state = "on" if device.motion_detection else "off" echo(f"{device!s} motion detection set to {state}") return None @cli.command() @click.option( "--device-name", "-dn", required=False, default=None, help="Name of device, if ommited shows all devices", ) @pass_ring @click.pass_context async def show(ctx, ring: Ring, device_name): """Display ring devices.""" devices: Sequence[RingGeneric] | None = None if device_name and (device := ring.get_device_by_name(device_name)): devices = [device] elif device_name: echo( f"No device with name {device_name} found. " + "List of found device names (kind) is:" ) return await ctx.invoke(list_command) else: devices = ring.get_device_list() for dev in devices: dev.update_health_data() echo("Name: %s" % dev.name) echo("Family: %s" % dev.family) echo("ID: %s" % dev.id) echo("Timezone: %s" % dev.timezone) echo("Wifi Name: %s" % dev.wifi_name) echo("Wifi RSSI: %s" % dev.wifi_signal_strength) echo() return None @cli.command(name="devices") @click.option( "--device-name", "-dn", required=False, default=None, help="Name of device, if ommited shows all devices", ) @click.option( "--json", "json_flag", required=False, is_flag=True, help="Output raw json", ) @pass_ring @click.pass_context async def devices_command(ctx, ring: Ring, device_name, json_flag): """Get device information.""" if not json_flag: echo( "(Pretty format coming soon, if you want json consistently " + "from this command provide the --json flag)" ) device_json = None if device_name and (device := ring.get_device_by_name(device_name)): device_json = ring.devices_data[device.family][device.id] elif device_name: echo( f"No device with name {device_name} found. " + "List of found device names (kind) is:" ) return await ctx.invoke(list_command) if device_json: echo(json.dumps(device_json, indent=2)) return None else: for device_type in ring.devices_data: for device_api_id in ring.devices_data[device_type]: echo( json.dumps(ring.devices_data[device_type][device_api_id], indent=2) ) return None @cli.command() @click.option( "--json", "json_flag", required=False, is_flag=True, help="Output raw json", ) @pass_ring async def dings(ring: Ring, json_flag) -> None: """Get dings information.""" if not json_flag: echo( "(Pretty format coming soon, if you want json consistently " + "from this command provide the --json flag)" ) echo(json.dumps(ring.dings_data, indent=2)) @cli.command() @click.option( "--json", "json_flag", required=False, is_flag=True, help="Output raw json", ) @pass_ring async def groups(ring: Ring, json_flag) -> None: """Get group information.""" if not json_flag: echo( "(Pretty format coming soon, if you want json consistently " + "from this command provide the --json flag)" ) if not ring.groups_data: echo("No ring device groups setup") else: for light_group in ring.groups().values(): light_group.update() echo(json.dumps(light_group._attrs, indent=2)) echo(json.dumps(light_group._health_attrs, indent=2)) @cli.command() @click.option( "--url", required=True, type=str, help="Url to query, i.e. /clients_api/dings/active", ) @pass_ring async def raw_query(ring: Ring, url) -> None: """Directly query a url and return json result.""" data = ring.query(url).json() echo(json.dumps(data, indent=2)) @cli.command(name="history") @click.option( "--device-name", "-dn", required=False, default=None, help="Name of device, if ommited shows all devices", ) @click.option( "--limit", required=False, default=5, help="Limit number of records to return", ) @click.option( "--kind", required=False, default=None, type=click.Choice(["ding", "motion", "on_demand"], case_sensitive=False), help="Get devices", ) @click.option( "--json", "json_flag", required=False, is_flag=True, help="Output raw json", ) @pass_ring @click.pass_context async def history_command(ctx, ring: Ring, device_name, kind, limit, json_flag): """Print raw json.""" if not json_flag: echo( "(Pretty format coming soon, if you want json consistently " + "from this command provide the --json flag)" ) device = ring.get_device_by_name(device_name) if not device: echo( f"No device with name {device_name} found. " + "List of found device names (kind) is:" ) return await ctx.invoke(list_command) history = device.history(limit=limit, kind=kind, convert_timezone=False) echo(json.dumps(history, indent=2)) return None @cli.command() @click.option( "--count", required=False, default=False, is_flag=True, help="Count the number of videos on your Ring account", ) @click.option( "--download-all", required=False, default=False, is_flag=True, help="Download all videos on your Ring account", ) @click.option( "--download", required=False, default=False, is_flag=True, help="Download videos on your Ring account up to the max-count option", ) @click.option( "--max-count", required=False, default=300, help="Maximum count of videos to count or download from your Ring account", ) @click.option( "--download-to", required=False, default="./", help="Download location ending with a /", ) @click.option( "--device-name", "-dn", default=None, required=False, help="Name of the ring device, if ommited uses the first device returned", ) @pass_ring @click.pass_context async def videos( ctx, ring: Ring, count, download, download_all, max_count, download_to, device_name ): """Interact with ring videos.""" device = None if device_name and not (device := ring.get_video_device_by_name(device_name)): echo( f"No device with name {device_name} found. " + "List of found device names (kind) is:" ) return await ctx.invoke(list_command) if device and not device.has_capability("video"): echo(f"Device {device.name} is not a video device") return None # return the first device is implemented to be consistent with previous cli version if not device: if video_devices := ring.video_devices(): device = video_devices[0] else: echo( "No video devices found. " + "List of found device names (with device kind) is:" ) return await ctx.invoke(list_command) if not device: # Make mypy happy return None if ( not count and not download and not download_all and device.last_recording_id and (url := device.recording_url(device.last_recording_id)) ): echo("Last recording url is: " + url) return None events = None if download_all: download = True max_count = -1 def _get_events(device, max_count): limit = 100 if max_count == -1 else min(100, max_count) events = [] history = device.history(limit=limit) while len(history) > 0: events += history if (len(events) >= max_count and max_count != -1) or len(history) < limit: break history = device.history(older_than=history[-1]["id"], limit=limit) return events if count: echo( f"\tCounting videos linked on your Ring account for {device.name}.\n" + "\tThis may take some time....\n" ) events = _get_events(device, max_count) motion = len([m["kind"] for m in events if m["kind"] == "motion"]) ding = len([m["kind"] for m in events if m["kind"] == "ding"]) on_demand = len([m["kind"] for m in events if m["kind"] == "on_demand"]) echo(f"\tTotal videos: {len(events)}") echo(f"\tDing triggered: {ding}") echo(f"\tMotion triggered: {motion}") echo(f"\tOn-Demand triggered: {on_demand}") if download: if events is None: echo( "\tGetting videos linked on your Ring account.\n" "\tThis may take some time....\n" ) events = _get_events(device, max_count) echo( f"\tDownloading {len(events)} videos linked on your Ring account.\n" "\tThis may take some time....\n" ) for counter, event in enumerate(events): filename = str(PurePath(download_to, _format_filename(device.name, event))) echo(f"\t{counter}/{len(events)} Downloading {filename}") device.recording_download(event["id"], filename=filename, override=False) return None return None async def ainput(string: str): loop = asyncio.get_event_loop() await loop.run_in_executor(None, lambda s=string: sys.stdout.write(s + " ")) # type: ignore[misc] def read_with_timeout(timeout): if select.select( [ sys.stdin, ], [], [], timeout, )[0]: # line = sys.stdin.next() return sys.stdin.readline() return None line = None while loop.is_running() and not line: line = await loop.run_in_executor(None, functools.partial(read_with_timeout, 1)) return line def get_now_str(): return str(datetime.utcnow()) class _event_handler: # pylint:disable=invalid-name def __init__(self, ring: Ring) -> None: self.ring = ring def on_event(self, event: RingEvent) -> None: msg = ( get_now_str() + ": " + str(event) + " : Currently active count = " + str(len(self.ring.push_dings_data)) ) echo(msg) @cli.command @click.option( "--credentials-file", required=False, default=None, help=( "File to store push credentials, " + "if not provided credentials will be recreated from scratch" ), ) @click.option( "--store-credentials/--no-store-credentials", default=True, help="Whether or not to store the push credentials, default is false", ) @click.option( "--show-credentials", default=False, is_flag=True, help="Whether or not to show the push credentials, default is false", ) @pass_ring @click.pass_context async def listen( ctx, ring, store_credentials, credentials_file, show_credentials, ) -> None: """Listen to push notification like the ones sent to your phone.""" if not can_listen: echo("Ring is not configured for listening to notifications!") echo("pip install ring_doorbell[listen]") return from ring_doorbell.listen import ( # pylint:disable=import-outside-toplevel RingEventListener, ) def credentials_updated_callback(credentials) -> None: if store_credentials: with open(credentials_file, "w", encoding="utf-8") as f: json.dump(credentials, f) else: echo("New push credentials created:") if show_credentials: echo(credentials) if not credentials_file: credentials_file = gcm_cache_file else: credentials_file = Path(credentials_file) credentials = None if store_credentials and credentials_file.is_file(): # already registered, load previous credentials with open(credentials_file, encoding="utf-8") as f: credentials = json.load(f) event_listener = RingEventListener(ring, credentials, credentials_updated_callback) event_listener.start() event_listener.add_notification_callback(_event_handler(ring).on_event) await ainput("Listening, press enter to cancel\n") event_listener.stop() if __name__ == "__main__": cli() # pylint: disable=no-value-for-parameter python-ring-doorbell-0.8.12/ring_doorbell/const.py000066400000000000000000000165371463726264600222350ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Constants and enums.""" from __future__ import annotations from enum import Enum, auto from typing import Final from ring_doorbell.exceptions import RingError class OAuth: """OAuth class constants.""" ENDPOINT = "https://oauth.ring.com/oauth/token" CLIENT_ID = "ring_official_android" SCOPE: Final[list[str]] = ["client"] class RingEventKind(Enum): """Enum of available ring events.""" DING = "ding" MOTION = "motion" INTERCOM_UNLOCK = "intercom_unlock" class RingCapability(Enum): """Enum of available ring events.""" VIDEO = auto() MOTION_DETECTION = auto() HISTORY = auto() LIGHT = auto() SIREN = auto() VOLUME = auto() BATTERY = auto() OPEN = auto() KNOCK = auto() PRE_ROLL = auto() @staticmethod def from_name(name: str) -> RingCapability: """Return ring capability from string value.""" capability = name.replace("-", "_").upper() for ring_capability in RingCapability: if ring_capability.name == capability: return ring_capability msg = f"Unknown ring capability {name}" raise RingError(msg) PACKAGE_NAME = "ring_doorbell" # timeout for HTTP requests TIMEOUT = 10 # longer default timeout for recording downloads - typical video file sizes # are ~12 MB and empirical testing reveals a ~20 second download time over a # fast connection, suggesting speed is largely governed by capacity of Ring # backend; to be safe, we factor in a worst case overhead and set it to 2 # minutes (this default can be overridden in method call) DEFAULT_VIDEO_DOWNLOAD_TIMEOUT = 120 # API endpoints API_VERSION = "11" API_URI = "https://api.ring.com" USER_AGENT = "android:com.ringapp" # random uuid, used to make a hardware id that doesn't change or clash NAMESPACE_UUID = "379378b0-f747-4b67-a10f-3b13327e8879" DEFAULT_LISTEN_EVENT_EXPIRES_IN = 180 # for Ring android app. 703521446232 for ring-site RING_SENDER_ID = 876313859327 CLI_TOKEN_FILE = "ring_token.cache" # noqa: S105 GCM_TOKEN_FILE = "ring_gcm_token.cache" # noqa: S105 CHIMES_ENDPOINT = "/clients_api/chimes/{0}" DEVICES_ENDPOINT = "/clients_api/ring_devices" DINGS_ENDPOINT = "/clients_api/dings/active" DOORBELLS_ENDPOINT = "/clients_api/doorbots/{0}" PERSIST_TOKEN_ENDPOINT = "/clients_api/device" # noqa: S105 SUBSCRIPTION_ENDPOINT = "/clients_api/device" GROUPS_ENDPOINT = "/groups/v1/locations/{0}/groups" LOCATIONS_HISTORY_ENDPOINT = "/evm/v2/history/locations/{0}" LOCATIONS_ENDPOINT = "/clients_api/locations/{0}" HEALTH_DOORBELL_ENDPOINT = DOORBELLS_ENDPOINT + "/health" HEALTH_CHIMES_ENDPOINT = CHIMES_ENDPOINT + "/health" LIGHTS_ENDPOINT = DOORBELLS_ENDPOINT + "/floodlight_light_{1}" LINKED_CHIMES_ENDPOINT = CHIMES_ENDPOINT + "/linked_doorbots" LIVE_STREAMING_ENDPOINT = DOORBELLS_ENDPOINT + "/live_view" NEW_SESSION_ENDPOINT = "/clients_api/session" RINGTONES_ENDPOINT = "/ringtones" SIREN_ENDPOINT = DOORBELLS_ENDPOINT + "/siren_{1}" SNAPSHOT_ENDPOINT = "/clients_api/snapshots/image/{0}" SNAPSHOT_TIMESTAMP_ENDPOINT = "/clients_api/snapshots/timestamps" TESTSOUND_CHIME_ENDPOINT = CHIMES_ENDPOINT + "/play_sound" URL_DOORBELL_HISTORY = DOORBELLS_ENDPOINT + "/history" URL_RECORDING = "/clients_api/dings/{0}/recording" URL_RECORDING_SHARE_PLAY = "/clients_api/dings/{0}/share/play" GROUP_DEVICES_ENDPOINT = GROUPS_ENDPOINT + "/{1}/devices" SETTINGS_ENDPOINT = "/devices/v1/devices/{0}/settings" # Alternative API for Intercom history, not used in favor of the DoorBell API URL_INTERCOM_HISTORY = LOCATIONS_HISTORY_ENDPOINT + "?ringtercom" INTERCOM_OPEN_ENDPOINT = "/commands/v1/devices/{0}/device_rpc" INTERCOM_INVITATIONS_ENDPOINT = LOCATIONS_ENDPOINT + "/invitations" INTERCOM_INVITATIONS_DELETE_ENDPOINT = LOCATIONS_ENDPOINT + "/invitations/{1}" INTERCOM_ALLOWED_USERS = LOCATIONS_ENDPOINT + "/users" KIND_DING = "ding" KIND_MOTION = "motion" KIND_INTERCOM_UNLOCK = "intercom_unlock" # chime test sound kinds CHIME_TEST_SOUND_KINDS = (KIND_DING, KIND_MOTION) # default values CHIME_VOL_MIN = 0 CHIME_VOL_MAX = 10 DOORBELL_VOL_MIN = 0 DOORBELL_VOL_MAX = 11 MIC_VOL_MIN = 0 MIC_VOL_MAX = 11 VOICE_VOL_MIN = 0 VOICE_VOL_MAX = 11 OTHER_DOORBELL_VOL_MIN = 0 OTHER_DOORBELL_VOL_MAX = 8 DOORBELL_EXISTING_TYPE = {0: "Mechanical", 1: "Digital", 2: "Not Present"} SIREN_DURATION_MIN = 0 SIREN_DURATION_MAX = 120 # device model kinds CHIME_KINDS = ["chime", "chime_v2"] CHIME_PRO_KINDS = ["chime_pro", "chime_pro_v2"] DOORBELL_KINDS = ["doorbot", "doorbell", "doorbell_v3"] DOORBELL_2_KINDS = ["doorbell_v4", "doorbell_v5"] DOORBELL_3_KINDS = ["doorbell_scallop_lite"] DOORBELL_4_KINDS = ["doorbell_oyster"] # Added DOORBELL_3_PLUS_KINDS = ["doorbell_scallop"] DOORBELL_PRO_KINDS = ["lpd_v1", "lpd_v2", "lpd_v3"] DOORBELL_PRO_2_KINDS = ["lpd_v4"] DOORBELL_ELITE_KINDS = ["jbox_v1"] DOORBELL_WIRED_KINDS = ["doorbell_graham_cracker"] PEEPHOLE_CAM_KINDS = ["doorbell_portal"] DOORBELL_GEN2_KINDS = ["cocoa_doorbell", "cocoa_doorbell_v2"] FLOODLIGHT_CAM_KINDS = ["hp_cam_v1", "floodlight_v2"] FLOODLIGHT_CAM_PRO_KINDS = ["floodlight_pro"] FLOODLIGHT_CAM_PLUS_KINDS = ["cocoa_floodlight"] INDOOR_CAM_KINDS = ["stickup_cam_mini"] INDOOR_CAM_GEN2_KINDS = ["stickup_cam_mini_v2"] SPOTLIGHT_CAM_BATTERY_KINDS = ["stickup_cam_v4"] SPOTLIGHT_CAM_WIRED_KINDS = ["hp_cam_v2", "spotlightw_v2"] SPOTLIGHT_CAM_PLUS_KINDS = ["cocoa_spotlight"] SPOTLIGHT_CAM_PRO_KINDS = ["stickup_cam_longfin"] STICKUP_CAM_KINDS = ["stickup_cam", "stickup_cam_v3"] STICKUP_CAM_BATTERY_KINDS = ["stickup_cam_lunar"] STICKUP_CAM_ELITE_KINDS = ["stickup_cam_elite", "stickup_cam_wired"] STICKUP_CAM_WIRED_KINDS = STICKUP_CAM_ELITE_KINDS # Deprecated STICKUP_CAM_GEN3_KINDS = ["cocoa_camera"] BEAM_KINDS = ["beams_ct200_transformer"] INTERCOM_KINDS = ["intercom_handset_audio"] # error strings MSG_BOOLEAN_REQUIRED = "Boolean value is required." MSG_EXISTING_TYPE = f"Integer value where {DOORBELL_EXISTING_TYPE}." MSG_GENERIC_FAIL = "Sorry.. Something went wrong..." FILE_EXISTS = "The file {0} already exists." MSG_VOL_OUTBOUND = "Must be within the {0}-{1}." MSG_ALLOWED_VALUES = "Only the following values are allowed: {0}." MSG_EXPECTED_ATTRIBUTE_NOT_FOUND = "Couldn't find expected attribute: {0}." PUSH_ACTION_DING = "com.ring.push.HANDLE_NEW_DING" PUSH_ACTION_MOTION = "com.ring.push.HANDLE_NEW_motion" PUSH_ACTION_INTERCOM_UNLOCK = "com.ring.push.INTERCOM_UNLOCK_FROM_APP" POST_DATA_JSON = { "api_version": API_VERSION, "device_model": "ring-doorbell", } POST_DATA = { "api_version": API_VERSION, "device[os]": "android", "device[app_brand]": "ring", "device[metadata][device_model]": "KVM", "device[metadata][device_name]": "Python", "device[metadata][resolution]": "600x800", "device[metadata][app_version]": "1.3.806", "device[metadata][app_instalation_date]": "", "device[metadata][manufacturer]": "Qemu", "device[metadata][device_type]": "desktop", "device[metadata][architecture]": "desktop", "device[metadata][language]": "en", } PERSIST_TOKEN_DATA = { "api_version": API_VERSION, "device[metadata][device_model]": "KVM", "device[metadata][device_name]": "Python", "device[metadata][resolution]": "600x800", "device[metadata][app_version]": "1.3.806", "device[metadata][app_instalation_date]": "", "device[metadata][manufacturer]": "Qemu", "device[metadata][device_type]": "desktop", "device[metadata][architecture]": "x86", "device[metadata][language]": "en", } python-ring-doorbell-0.8.12/ring_doorbell/doorbot.py000066400000000000000000000353551463726264600225560ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Doorbell wrapper.""" from __future__ import annotations import logging import time from pathlib import Path from typing import TYPE_CHECKING, Any from ring_doorbell.const import ( DEFAULT_VIDEO_DOWNLOAD_TIMEOUT, DINGS_ENDPOINT, DOORBELL_2_KINDS, DOORBELL_3_KINDS, DOORBELL_3_PLUS_KINDS, DOORBELL_4_KINDS, DOORBELL_ELITE_KINDS, DOORBELL_EXISTING_TYPE, DOORBELL_GEN2_KINDS, DOORBELL_KINDS, DOORBELL_PRO_2_KINDS, DOORBELL_PRO_KINDS, DOORBELL_VOL_MAX, DOORBELL_VOL_MIN, DOORBELL_WIRED_KINDS, DOORBELLS_ENDPOINT, FILE_EXISTS, HEALTH_DOORBELL_ENDPOINT, LIVE_STREAMING_ENDPOINT, MSG_ALLOWED_VALUES, MSG_BOOLEAN_REQUIRED, MSG_EXISTING_TYPE, MSG_EXPECTED_ATTRIBUTE_NOT_FOUND, MSG_VOL_OUTBOUND, PEEPHOLE_CAM_KINDS, SETTINGS_ENDPOINT, SNAPSHOT_ENDPOINT, SNAPSHOT_TIMESTAMP_ENDPOINT, URL_RECORDING, URL_RECORDING_SHARE_PLAY, RingCapability, ) from ring_doorbell.exceptions import RingError from ring_doorbell.generic import RingGeneric _LOGGER = logging.getLogger(__name__) class RingDoorBell(RingGeneric): """Implementation for Ring Doorbell.""" if TYPE_CHECKING: from ring_doorbell.ring import Ring def __init__(self, ring: Ring, device_api_id: int, *, shared: bool = False) -> None: """Initialise the doorbell.""" super().__init__(ring, device_api_id) self.shared = shared @property def family(self) -> str: """Return Ring device family type.""" return "authorized_doorbots" if self.shared else "doorbots" def update_health_data(self) -> None: """Update health attrs.""" self._health_attrs = ( self._ring.query(HEALTH_DOORBELL_ENDPOINT.format(self.device_api_id)) .json() .get("device_health", {}) ) @property def model(self) -> str: # noqa: C901, PLR0911 """Return Ring device model name.""" if self.kind in DOORBELL_KINDS: return "Doorbell" if self.kind in DOORBELL_2_KINDS: return "Doorbell 2" if self.kind in DOORBELL_3_KINDS: return "Doorbell 3" if self.kind in DOORBELL_3_PLUS_KINDS: return "Doorbell 3 Plus" if self.kind in DOORBELL_4_KINDS: return "Doorbell 4" if self.kind in DOORBELL_PRO_KINDS: return "Doorbell Pro" if self.kind in DOORBELL_PRO_2_KINDS: return "Doorbell Pro 2" if self.kind in DOORBELL_ELITE_KINDS: return "Doorbell Elite" if self.kind in DOORBELL_WIRED_KINDS: return "Doorbell Wired" if self.kind in DOORBELL_GEN2_KINDS: return "Doorbell (2nd Gen)" if self.kind in PEEPHOLE_CAM_KINDS: return "Peephole Cam" return "Unknown Doorbell" def has_capability(self, capability: RingCapability | str) -> bool: # noqa: PLR0911 """Return if device has specific capability.""" capability = ( capability if isinstance(capability, RingCapability) else RingCapability.from_name(capability) ) if capability == RingCapability.BATTERY: return self.kind in ( DOORBELL_KINDS + DOORBELL_2_KINDS + DOORBELL_3_KINDS + DOORBELL_3_PLUS_KINDS + DOORBELL_4_KINDS + DOORBELL_GEN2_KINDS + PEEPHOLE_CAM_KINDS ) if capability == RingCapability.KNOCK: return self.kind in PEEPHOLE_CAM_KINDS if capability == RingCapability.PRE_ROLL: return self.kind in DOORBELL_3_PLUS_KINDS if capability == RingCapability.VOLUME: return True if capability == RingCapability.HISTORY: return True if capability in [RingCapability.MOTION_DETECTION, RingCapability.VIDEO]: return self.kind in ( DOORBELL_KINDS + DOORBELL_2_KINDS + DOORBELL_3_KINDS + DOORBELL_3_PLUS_KINDS + DOORBELL_4_KINDS + DOORBELL_PRO_KINDS + DOORBELL_PRO_2_KINDS + DOORBELL_WIRED_KINDS + DOORBELL_GEN2_KINDS + PEEPHOLE_CAM_KINDS ) return False @property def battery_life(self) -> int | None: """Return battery life.""" if ( bl1 := self._attrs.get("battery_life") ) is None and "battery_life_2" not in self._attrs: return None value = 0 if bl1: value += int(bl1) if bl2 := self._attrs.get("battery_life_2"): # Camera has two battery bays value += int(bl2) if value > 100: value = 100 return value def _get_chime_setting(self, setting: str) -> Any | None: if (settings := self._attrs.get("settings")) and ( chime_settings := settings.get("chime_settings") ): return chime_settings.get(setting) return None @property def existing_doorbell_type(self) -> str | None: """ Return existing doorbell type. 0: Mechanical 1: Digital 2: Not Present """ try: if (dtype := self._get_chime_setting("type")) is not None: return DOORBELL_EXISTING_TYPE[dtype] except AttributeError: return None else: return None @existing_doorbell_type.setter def existing_doorbell_type(self, value: int) -> None: """ Return existing doorbell type. 0: Mechanical 1: Digital 2: Not Present """ if value not in DOORBELL_EXISTING_TYPE: msg = f"value must be in {MSG_EXISTING_TYPE}" raise RingError(msg) params = { "doorbot[description]": self.name, "doorbot[settings][chime_settings][type]": value, } if self.existing_doorbell_type: url = DOORBELLS_ENDPOINT.format(self.device_api_id) self._ring.query(url, extra_params=params, method="PUT") self._ring.update_devices() @property def existing_doorbell_type_enabled(self) -> bool | None: """Return if existing doorbell type is enabled.""" if self.existing_doorbell_type: if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[2]: return None return self._get_chime_setting("enable") return False @existing_doorbell_type_enabled.setter def existing_doorbell_type_enabled(self, value: bool) -> None: """Enable/disable the existing doorbell if Digital/Mechanical.""" if self.existing_doorbell_type: if not isinstance(value, bool): raise RingError(MSG_BOOLEAN_REQUIRED) if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[2]: return params = { "doorbot[description]": self.name, "doorbot[settings][chime_settings][enable]": value, } url = DOORBELLS_ENDPOINT.format(self.device_api_id) self._ring.query(url, extra_params=params, method="PUT") self._ring.update_devices() @property def existing_doorbell_type_duration(self) -> int | None: """Return duration for Digital chime.""" if ( self.existing_doorbell_type and self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[1] ): return self._get_chime_setting("duration") return None @existing_doorbell_type_duration.setter def existing_doorbell_type_duration(self, value: int) -> None: """Set duration for Digital chime.""" if self.existing_doorbell_type: if not ( (isinstance(value, int)) and (DOORBELL_VOL_MIN <= value <= DOORBELL_VOL_MAX) ): raise RingError( MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN, DOORBELL_VOL_MAX) ) if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[1]: params = { "doorbot[description]": self.name, "doorbot[settings][chime_settings][duration]": value, } url = DOORBELLS_ENDPOINT.format(self.device_api_id) self._ring.query(url, extra_params=params, method="PUT") self._ring.update_devices() @property def last_recording_id(self) -> int | None: """Return the last recording ID.""" try: res = self.history(limit=1) return res[0].get("id") if res else None except (IndexError, TypeError): return None @property def live_streaming_json(self) -> dict[str, Any] | None: """Return JSON for live streaming.""" url = LIVE_STREAMING_ENDPOINT.format(self.device_api_id) req = self._ring.query(url, method="POST") if req and req.status_code == 200: url = DINGS_ENDPOINT try: return self._ring.query(url).json()[0] except (IndexError, TypeError): pass return None def recording_download( self, recording_id: int, filename: str | None = None, *, override: bool = False, timeout: int = DEFAULT_VIDEO_DOWNLOAD_TIMEOUT, ) -> bytes | None: """Save a recording in MP4 format to a file or return raw.""" if not self.has_subscription: msg = "Your Ring account does not have an active subscription." _LOGGER.warning(msg) return None url = URL_RECORDING.format(recording_id) try: # Video download needs a longer timeout to get the large video file req = self._ring.query(url, timeout=timeout) if req.status_code == 200: if filename: if Path(filename).is_file() and not override: raise RingError(FILE_EXISTS.format(filename)) with Path(filename).open("wb") as recording: recording.write(req.content) return None else: return req.content else: msg = ( f"Could not get recording at url {url}, " f"status code is {req.status_code}" ) raise RingError(msg) except OSError as error: msg = f"Error downloading recording {recording_id}: {error}" _LOGGER.exception(msg) raise RingError(msg) from error def recording_url(self, recording_id: int) -> str | None: """Return HTTPS recording URL.""" if not self.has_subscription: msg = "Your Ring account does not have an active subscription." _LOGGER.warning(msg) return None url = URL_RECORDING_SHARE_PLAY.format(recording_id) req = self._ring.query(url) data = req.json() if req and req.status_code == 200 and data is not None: return data["url"] return None @property def subscribed(self) -> bool: """Return if is online.""" result = self._attrs.get("subscribed") if result is None: return False return True @property def subscribed_motion(self) -> bool: """Return if is subscribed_motion.""" result = self._attrs.get("subscribed_motions") if result is None: return False return True @property def has_subscription(self) -> bool: """Return boolean if the account has subscription.""" if features := self._attrs.get("features"): return features.get("show_recordings", False) return False @property def volume(self) -> int: """Return volume.""" return self._attrs["settings"].get("doorbell_volume", 0) @volume.setter def volume(self, value: int) -> None: if not ( (isinstance(value, int)) and (DOORBELL_VOL_MIN <= value <= DOORBELL_VOL_MAX) ): raise RingError(MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN, DOORBELL_VOL_MAX)) params = { "doorbot[description]": self.name, "doorbot[settings][doorbell_volume]": str(value), } url = DOORBELLS_ENDPOINT.format(self.device_api_id) self._ring.query(url, extra_params=params, method="PUT") self._ring.update_devices() @property def connection_status(self) -> str | None: """Return connection status.""" if alerts := self._attrs.get("alerts"): return alerts.get("connection") return None def get_snapshot( self, retries: int = 3, delay: int = 1, filename: str | None = None ) -> bytes | None: """Take a snapshot and download it.""" url = SNAPSHOT_TIMESTAMP_ENDPOINT payload = {"doorbot_ids": [self._attrs.get("id")]} self._ring.query(url, method="POST", json=payload) request_time = time.time() for _ in range(retries): time.sleep(delay) response = self._ring.query(url, method="POST", json=payload).json() if response["timestamps"][0]["timestamp"] / 1000 > request_time: snapshot = self._ring.query( SNAPSHOT_ENDPOINT.format(self._attrs.get("id")) ).content if filename: with Path(filename).open("wb") as jpg: jpg.write(snapshot) return None return snapshot return None def _motion_detection_state(self) -> bool | None: if settings := self._attrs.get("settings"): return settings.get("motion_detection_enabled") return None @property def motion_detection(self) -> bool: """Return motion detection enabled state.""" return state if (state := self._motion_detection_state()) else False @motion_detection.setter def motion_detection(self, state: bool) -> None: """Set the motion detection enabled state.""" values = [True, False] if state not in values: raise RingError(MSG_ALLOWED_VALUES.format("True, False")) if self._motion_detection_state() is None: _LOGGER.warning( "%s", MSG_EXPECTED_ATTRIBUTE_NOT_FOUND.format( "settings[motion_detection_enabled]" ), ) return url = SETTINGS_ENDPOINT.format(self.device_api_id) payload = {"motion_settings": {"motion_detection_enabled": state}} self._ring.query(url, method="PATCH", json=payload) self._ring.update_devices() python-ring-doorbell-0.8.12/ring_doorbell/event.py000066400000000000000000000011561463726264600222170ustar00rootroot00000000000000"""Module for ring events.""" from __future__ import annotations from dataclasses import dataclass from typing import Any @dataclass class RingEvent: """Class for ring events.""" id: int doorbot_id: int device_name: str device_kind: str now: float expires_in: float kind: str state: str def __getitem__(self, key: str) -> Any: """Get a value by string.""" return getattr(self, key) def get(self, key: str) -> Any | None: """Get a value by string and return None if not present.""" return getattr(self, key) if hasattr(self, key) else None python-ring-doorbell-0.8.12/ring_doorbell/exceptions.py000066400000000000000000000005671463726264600232640ustar00rootroot00000000000000"""ring-doorbell exceptions.""" class RingError(Exception): """Base exception for device errors.""" class Requires2FAError(RingError): """Exception that 2FA is required.""" class AuthenticationError(RingError): """Exception for ring authentication errors.""" class RingTimeout(RingError): # noqa: N818 """Exception for ring authentication errors.""" python-ring-doorbell-0.8.12/ring_doorbell/generic.py000066400000000000000000000165321463726264600225160ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring RingGeneric wrapper.""" # pylint: disable=useless-object-inheritance from __future__ import annotations import logging from datetime import datetime from typing import TYPE_CHECKING, Any import pytz from ring_doorbell.const import URL_DOORBELL_HISTORY, RingCapability _LOGGER = logging.getLogger(__name__) class RingGeneric: """Generic Implementation for Ring Chime/Doorbell.""" if TYPE_CHECKING: from ring_doorbell.ring import Ring def __init__(self, ring: Ring, device_api_id: int) -> None: """Initialize Ring Generic.""" self._ring = ring # This is the account ID of the device. # Not the same as device ID. self.device_api_id = device_api_id self.capability = False self.alert = None self._health_attrs: dict[str, Any] = {} self._last_history: list[dict[str, Any]] = [] # alerts notifications self.alert_expires_at = None def __repr__(self) -> str: """Return __repr__.""" return f"<{self.__class__.__name__}: {self.name}>" def __str__(self) -> str: """Return string representation of device.""" return f"{self.name} ({self.kind})" def update(self) -> None: """Update this device info.""" self.update_health_data() def update_health_data(self) -> None: """Update the health data.""" raise NotImplementedError @property def _attrs(self) -> dict[str, Any]: """Return attributes.""" return self._ring.devices_data[self.family][self.device_api_id] @property def id(self) -> int: """Return ID.""" return self.device_api_id @property def name(self) -> str: """Return name.""" return self._attrs["description"] @property def device_id(self) -> str: """Return device ID. This is the device_id returned by the api, usually the MAC. Not to be confused with the id for the device """ return self._attrs["device_id"] @property def location_id(self) -> str | None: """Return location id.""" return self._attrs.get("location_id", None) @property def family(self) -> str: """Return Ring device family type.""" raise NotImplementedError @property def model(self) -> str: """Return Ring device model name.""" raise NotImplementedError @property def battery_life(self) -> int | None: """Return battery life.""" raise NotImplementedError def has_capability(self, capability: RingCapability | str) -> bool: # noqa: ARG002 """Return if device has specific capability.""" return self.capability @property def address(self) -> str | None: """Return address.""" return self._attrs.get("address") @property def firmware(self) -> str | None: """Return firmware.""" return self._attrs.get("firmware_version") @property def latitude(self) -> float | None: """Return latitude attr.""" return self._attrs.get("latitude") @property def longitude(self) -> float | None: """Return longitude attr.""" return self._attrs.get("longitude") @property def kind(self) -> str: """Return kind attr.""" return self._attrs["kind"] @property def timezone(self) -> str | None: """Return timezone.""" return self._attrs.get("time_zone") @property def wifi_name(self) -> str | None: """Return wifi ESSID name. Requires health data to be updated. """ return self._health_attrs.get("wifi_name") @property def wifi_signal_strength(self) -> int | None: """Return wifi RSSI. Requires health data to be updated. """ return self._health_attrs.get("latest_signal_strength") @property def wifi_signal_category(self) -> str | None: """Return wifi signal category. Requires health data to be updated. """ return self._health_attrs.get("latest_signal_category") @property def last_history(self) -> list[dict[str, Any]]: """Return the result of the last history query.""" return self._last_history def history( # noqa: C901, PLR0912, PLR0913 self, *, limit: int = 30, timezone: str | None = None, kind: str | None = None, enforce_limit: bool = False, older_than: int | None = None, retry: int = 8, convert_timezone: bool = True, ) -> list[dict[str, Any]]: """ Return history with datetime objects. :param limit: specify number of objects to be returned :param timezone: determine which timezone to convert data objects :param kind: filter by kind (ding, motion, on_demand) :param enforce_limit: when True, this will enforce the limit and kind :param older_than: return older objects than the passed event_id :param retry: determine the max number of attempts to archive the limit """ if not self.has_capability("history"): return [] queries = 0 original_limit = limit # set cap for max queries # pylint:disable=consider-using-min-builtin if retry > 10: retry = 10 while True: params = {"limit": limit} if older_than: params["older_than"] = older_than url = URL_DOORBELL_HISTORY.format(self.device_api_id) response = self._ring.query(url, extra_params=params).json() # cherrypick only the selected kind events if kind: response = list(filter(lambda array: array["kind"] == kind, response)) if convert_timezone: # convert for specific timezone utc = pytz.utc if timezone: mytz = pytz.timezone(timezone) for entry in response: dt_at = datetime.strptime( entry["created_at"], "%Y-%m-%dT%H:%M:%S.%f%z" ) utc_dt = datetime( dt_at.year, dt_at.month, dt_at.day, dt_at.hour, dt_at.minute, dt_at.second, tzinfo=utc, ) if timezone: tz_dt = utc_dt.astimezone(mytz) entry["created_at"] = tz_dt else: entry["created_at"] = utc_dt if enforce_limit: # return because already matched the number # of events by kind if len(response) >= original_limit: return response[:original_limit] # ensure the loop will exit after max queries queries += 1 if queries == retry: _LOGGER.debug( "Could not find total of %s of kind %s", original_limit, kind ) break # ensure the kind objects returned to match limit limit = limit * 2 else: break self._last_history = response return self._last_history python-ring-doorbell-0.8.12/ring_doorbell/group.py000066400000000000000000000066221463726264600222350ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring light group wrapper.""" from __future__ import annotations import logging import warnings from typing import TYPE_CHECKING, Any from ring_doorbell.const import ( GROUP_DEVICES_ENDPOINT, MSG_ALLOWED_VALUES, RingCapability, ) from ring_doorbell.exceptions import RingError _LOGGER = logging.getLogger(__name__) class RingLightGroup: """Implementation for RingLightGroup.""" if TYPE_CHECKING: from ring_doorbell.ring import Ring def __init__(self, ring: Ring, group_id: str) -> None: """Initialize Ring Light Group.""" self._ring = ring self.group_id = group_id # pylint:disable=invalid-name self._health_attrs: dict[str, Any] = {} self._health_attrs_fetched = False def __repr__(self) -> str: """Return __repr__.""" return f"<{self.__class__.__name__}: {self.name}>" def update(self) -> None: """Update this device info.""" url = GROUP_DEVICES_ENDPOINT.format(self.location_id, self.group_id) self._health_attrs = self._ring.query(url).json() self._health_attrs_fetched = True @property def _attrs(self) -> dict[str, Any]: """Return attributes.""" return self._ring.groups_data[self.group_id] @property def id(self) -> str: """Return ID.""" return self.group_id @property def name(self) -> str: """Return name.""" return self._attrs["name"] @property def family(self) -> str: """Return Ring device family type.""" return "group" @property def device_id(self) -> str: """Return group ID. Deprecated.""" warnings.warn( "RingLightGroup.device_id is deprecated; use group_id", DeprecationWarning, stacklevel=1, ) return self.group_id @property def location_id(self) -> str: """Return group location ID.""" return self._attrs["location_id"] @property def model(self) -> str: """Return Ring device model name.""" return "Light Group" def has_capability(self, capability: RingCapability | str) -> bool: """Return if device has specific capability.""" capability = ( capability if isinstance(capability, RingCapability) else RingCapability.from_name(capability) ) if capability == RingCapability.LIGHT: return True return False @property def lights(self) -> bool: """Return lights status.""" if not self._health_attrs_fetched: self.update() return self._health_attrs["lights_on"] @lights.setter def lights(self, value: bool | tuple[bool, int]) -> None: """Control the lights.""" values = ["True", "False"] state = None duration = None if isinstance(value, tuple): state, duration = value else: state = value if not isinstance(state, bool): raise RingError(MSG_ALLOWED_VALUES.format(", ".join(values))) url = GROUP_DEVICES_ENDPOINT.format(self.location_id, self.group_id) payload: dict[str, dict[str, bool | int]] = {"lights_on": {"enabled": state}} if duration is not None: payload["lights_on"]["duration_seconds"] = duration self._ring.query(url, method="POST", json=payload) self.update() python-ring-doorbell-0.8.12/ring_doorbell/listen/000077500000000000000000000000001463726264600220175ustar00rootroot00000000000000python-ring-doorbell-0.8.12/ring_doorbell/listen/__init__.py000066400000000000000000000006201463726264600241260ustar00rootroot00000000000000"""Package for listener modules.""" import sys try: from .eventlistener import RingEventListener from .listenerconfig import RingEventListenerConfig can_listen = sys.version_info >= (3, 9) # pylint:disable=invalid-name except ImportError: # pragma: no cover can_listen = False # pylint:disable=invalid-name __all__ = [ "RingEventListener", "RingEventListenerConfig", ] python-ring-doorbell-0.8.12/ring_doorbell/listen/eventlistener.py000066400000000000000000000207111463726264600252610ustar00rootroot00000000000000"""Module for listening to firebase cloud messages and updating dings.""" from __future__ import annotations import json import logging import time from datetime import datetime from typing import TYPE_CHECKING, Any, Callable, Dict from firebase_messaging import FcmPushClient from ring_doorbell.const import ( API_URI, API_VERSION, DEFAULT_LISTEN_EVENT_EXPIRES_IN, KIND_DING, KIND_INTERCOM_UNLOCK, KIND_MOTION, PUSH_ACTION_DING, PUSH_ACTION_INTERCOM_UNLOCK, PUSH_ACTION_MOTION, RING_SENDER_ID, SUBSCRIPTION_ENDPOINT, ) from ring_doorbell.event import RingEvent from ring_doorbell.exceptions import RingError from .listenerconfig import RingEventListenerConfig if TYPE_CHECKING: import asyncio from ring_doorbell.ring import Ring _logger = logging.getLogger(__name__) OnNotificationCallable = Callable[[RingEvent], None] CredentialsUpdatedCallable = Callable[[Dict[str, Any]], None] class RingEventListener: """Class to connect to firebase cloud messaging.""" def __init__( self, ring: Ring, credentials: dict[str, Any] | None = None, credentials_updated_callback: CredentialsUpdatedCallable | None = None, *, config: RingEventListenerConfig | None = None, ) -> None: """Initialise the event listener with credentials. Provide a callback for when credentials are updated by FCM. """ self._ring = ring self._callbacks: dict[int, OnNotificationCallable] = {} self.subscribed = False self.started = False self._app_id = self._ring.auth.get_hardware_id() self._device_model = self._ring.auth.get_device_model() self._credentials = credentials self._credentials_updated_callback = credentials_updated_callback self._receiver: FcmPushClient | None = None self._config: RingEventListenerConfig = ( config or RingEventListenerConfig.default_config() ) self._subscription_counter = 1 self._intercom_unlock_counter: dict[int, int] = {} def add_subscription_to_ring(self, token: str) -> None: """Add subscription to ring.""" if not self._ring.session: self._ring.create_session() session_patch_data = { "device": { "metadata": { "api_version": API_VERSION, "device_model": self._device_model, "pn_service": "fcm", }, "os": "android", "push_notification_token": token, } } resp = self._ring.auth.query( API_URI + SUBSCRIPTION_ENDPOINT, method="PATCH", json=session_patch_data, raise_for_status=False, ) if resp.status_code != 204: _logger.error( "Unable to checkin to listen service, " "response was %s %s, event listener not started", resp.status_code, resp.text, ) self.subscribed = False return self.subscribed = True # Update devices for the intercom unlock events if not self._ring.devices_data: self._ring.update_devices() if not self._ring.dings_data: self._ring.update_dings() def add_notification_callback(self, callback: OnNotificationCallable) -> int: """Add a callback to be notified on event.""" sub_id = self._subscription_counter self._callbacks[sub_id] = callback self._subscription_counter += 1 return sub_id def remove_notification_callback(self, subscription_id: int) -> None: """Remove a notification callback by id.""" if subscription_id == 1: msg = "Cannot remove the default callback for ring-doorbell with value 1" raise RingError(msg) if subscription_id not in self._callbacks: msg = f"ID {subscription_id} is not a valid callback id" raise RingError(msg) del self._callbacks[subscription_id] if len(self._callbacks) == 0 and self._receiver: self._receiver.stop() self._receiver = None def stop(self) -> None: """Stop the listener.""" if self._receiver: self.started = False self._receiver.stop() self._receiver = None self._callbacks = {} def start( self, callback: OnNotificationCallable | None = None, *, listen_loop: asyncio.AbstractEventLoop | None = None, callback_loop: asyncio.AbstractEventLoop | None = None, timeout: int = 30, ) -> bool: """Start the listener.""" if not callback: callback = self._ring._add_event_to_dings_data # noqa: SLF001 if not self._receiver: self._receiver = FcmPushClient( credentials=self._credentials, credentials_updated_callback=self._credentials_updated_callback, config=self._config, ) fcm_token = self._receiver.checkin(RING_SENDER_ID, self._app_id) if not fcm_token: _logger.error("Unable to check in to fcm, event listener not started") return False self.add_subscription_to_ring(fcm_token) if self.subscribed: self.add_notification_callback(callback) self._receiver.start( self._on_notification, listen_event_loop=listen_loop, callback_event_loop=callback_loop, ) start = time.time() now = start while not self._receiver.is_started() and now - start < timeout: time.sleep(0.1) now = time.time() self.started = self._receiver.is_started() return self.subscribed and self.started def _get_ding_event(self, gcm_data: dict[str, Any]) -> RingEvent: ding = gcm_data["ding"] action = gcm_data["action"] subtype = gcm_data["subtype"] if action.lower() == PUSH_ACTION_MOTION.lower(): kind = KIND_MOTION state = subtype elif action.lower == PUSH_ACTION_DING.lower(): kind = KIND_DING state = "ringing" else: kind = action state = subtype created_at = ding["created_at"] create_seconds = ( datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S.%f%z") ).timestamp() return RingEvent( id=ding["id"], kind=kind, doorbot_id=ding["doorbot_id"], device_name=ding["device_name"], device_kind=ding["device_kind"], now=create_seconds, expires_in=DEFAULT_LISTEN_EVENT_EXPIRES_IN, state=state, ) def _get_intercom_unlock_event(self, gcm_data: dict[str, Any]) -> RingEvent | None: device_api_id = gcm_data["alarm_meta"]["device_zid"] if (device := self._ring.get_device_by_api_id(device_api_id)) is None: _logger.debug("Event received for unknown device id: %s", device_api_id) return None if device_api_id not in self._intercom_unlock_counter: self._intercom_unlock_counter[device_api_id] = 0 self._intercom_unlock_counter[device_api_id] += 1 return RingEvent( id=self._intercom_unlock_counter[device_api_id], kind=KIND_INTERCOM_UNLOCK, doorbot_id=device_api_id, device_name=device.name, device_kind=device.kind, now=time.time(), expires_in=DEFAULT_LISTEN_EVENT_EXPIRES_IN, state="unlock", ) def _on_notification( self, notification: dict[str, dict[str, str]], persistent_id: str, # noqa: ARG002 obj: Any | None = None, # noqa: ARG002 ) -> None: gcm_data = json.loads(notification["data"]["gcmData"]) re: RingEvent | None = None if "ding" in gcm_data: re = self._get_ding_event(gcm_data) elif gcm_data.get("action") == PUSH_ACTION_INTERCOM_UNLOCK: re = self._get_intercom_unlock_event(gcm_data) elif "community_alert" not in gcm_data: _logger.debug( "Unexpected alert type in gcmData. Full message is:\n%s", json.dumps(notification), ) return if re: for callback in self._callbacks.values(): callback(re) python-ring-doorbell-0.8.12/ring_doorbell/listen/listenerconfig.py000066400000000000000000000010521463726264600254020ustar00rootroot00000000000000"""Module for RingEventListenerConfig.""" from __future__ import annotations from firebase_messaging import FcmPushClientConfig class RingEventListenerConfig(FcmPushClientConfig): """Configuration class for event listener.""" @staticmethod def default_config() -> RingEventListenerConfig: """Get an instance of the default config.""" config = RingEventListenerConfig() config.server_heartbeat_interval = 60 config.client_heartbeat_interval = 120 config.monitor_interval = 15 return config python-ring-doorbell-0.8.12/ring_doorbell/other.py000066400000000000000000000221151463726264600222150ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Other (Intercom) wrapper.""" from __future__ import annotations import json import logging import uuid from typing import TYPE_CHECKING, Any from ring_doorbell.const import ( DOORBELLS_ENDPOINT, HEALTH_DOORBELL_ENDPOINT, INTERCOM_ALLOWED_USERS, INTERCOM_INVITATIONS_DELETE_ENDPOINT, INTERCOM_INVITATIONS_ENDPOINT, INTERCOM_KINDS, INTERCOM_OPEN_ENDPOINT, MIC_VOL_MAX, MIC_VOL_MIN, MSG_VOL_OUTBOUND, OTHER_DOORBELL_VOL_MAX, OTHER_DOORBELL_VOL_MIN, SETTINGS_ENDPOINT, VOICE_VOL_MAX, VOICE_VOL_MIN, RingCapability, ) from ring_doorbell.exceptions import RingError from ring_doorbell.generic import RingGeneric _LOGGER = logging.getLogger(__name__) class RingOther(RingGeneric): """Implementation for Ring Intercom.""" if TYPE_CHECKING: from ring_doorbell.ring import Ring def __init__(self, ring: Ring, device_api_id: int, *, shared: bool = False) -> None: """Initialise the other devices.""" super().__init__(ring, device_api_id) self.shared = shared @property def family(self) -> str: """Return Ring device family type.""" return "other" def update_health_data(self) -> None: """Update health attrs.""" self._health_attrs = ( self._ring.query(HEALTH_DOORBELL_ENDPOINT.format(self.device_api_id)) .json() .get("device_health", {}) ) @property def model(self) -> str: """Return Ring device model name.""" if self.kind in INTERCOM_KINDS: return "Intercom" return "Unknown Other" def has_capability(self, capability: RingCapability | str) -> bool: """Return if device has specific capability.""" capability = ( capability if isinstance(capability, RingCapability) else RingCapability.from_name(capability) ) if capability in [RingCapability.OPEN, RingCapability.HISTORY]: return self.kind in INTERCOM_KINDS return False @property def battery_life(self) -> int | None: """Return battery life.""" if self.kind in INTERCOM_KINDS: if self._attrs.get("battery_life") is None: return None value = int(self._attrs.get("battery_life", 0)) if value and value > 100: value = 100 return value return None @property def subscribed(self) -> bool: """Return if is online.""" if self.kind in INTERCOM_KINDS: result = self._attrs.get("subscribed") if result is None: return False return True return False @property def has_subscription(self) -> bool: """Return boolean if the account has subscription.""" if self.kind in INTERCOM_KINDS and (features := self._attrs.get("features")): return features.get("show_recordings", False) return False @property def unlock_duration(self) -> str | None: """Return time unlock switch is held closed.""" return ( json.loads(self._attrs["settings"]["intercom_settings"]["config"]) .get("analog", {}) .get("unlock_duration") ) @property def doorbell_volume(self) -> int: """Return doorbell volume.""" if self.kind in INTERCOM_KINDS: return self._attrs["settings"].get("doorbell_volume", 0) return 0 @doorbell_volume.setter def doorbell_volume(self, value: int) -> None: if not ( (isinstance(value, int)) and (OTHER_DOORBELL_VOL_MIN <= value <= OTHER_DOORBELL_VOL_MAX) ): raise RingError( MSG_VOL_OUTBOUND.format(OTHER_DOORBELL_VOL_MIN, OTHER_DOORBELL_VOL_MAX) ) params = { "doorbot[settings][doorbell_volume]": str(value), } url = DOORBELLS_ENDPOINT.format(self.device_api_id) self._ring.query(url, extra_params=params, method="PUT") self._ring.update_devices() @property def keep_alive_auto(self) -> float | None: """The keep alive auto setting.""" if self.kind in INTERCOM_KINDS: return self._attrs["settings"].get("keep_alive_auto") return None @keep_alive_auto.setter def keep_alive_auto(self, value: float) -> None: """Update the keep alive auto setting.""" url = SETTINGS_ENDPOINT.format(self.device_api_id) payload = {"keep_alive_settings": {"keep_alive_auto": value}} self._ring.query(url, method="PATCH", json=payload) self._ring.update_devices() @property def mic_volume(self) -> int | None: """Return mic volume.""" if self.kind in INTERCOM_KINDS: return self._attrs["settings"].get("mic_volume") return None @mic_volume.setter def mic_volume(self, value: int) -> None: if not ((isinstance(value, int)) and (MIC_VOL_MIN <= value <= MIC_VOL_MAX)): raise RingError(MSG_VOL_OUTBOUND.format(MIC_VOL_MIN, MIC_VOL_MAX)) url = SETTINGS_ENDPOINT.format(self.device_api_id) payload = {"volume_settings": {"mic_volume": value}} self._ring.query(url, method="PATCH", json=payload) self._ring.update_devices() @property def voice_volume(self) -> int | None: """Return voice volume.""" if self.kind in INTERCOM_KINDS: return self._attrs["settings"].get("voice_volume") return None @voice_volume.setter def voice_volume(self, value: int) -> None: if not ((isinstance(value, int)) and (VOICE_VOL_MIN <= value <= VOICE_VOL_MAX)): raise RingError(MSG_VOL_OUTBOUND.format(VOICE_VOL_MIN, VOICE_VOL_MAX)) url = SETTINGS_ENDPOINT.format(self.device_api_id) payload = {"volume_settings": {"voice_volume": value}} self._ring.query(url, method="PATCH", json=payload) self._ring.update_devices() @property def clip_length_max(self) -> int | None: """Maximum clip length. This value sets an effective refractory period on consecutive rigns eg if set to default value of 60, rings occuring with 60 seconds of first will not be detected. """ url = SETTINGS_ENDPOINT.format(self.device_api_id) return ( self._ring.query(url, method="GET") .json() .get("video_settings", {}) .get("clip_length_max") ) @clip_length_max.setter def clip_length_max(self, value: int) -> None: url = SETTINGS_ENDPOINT.format(self.device_api_id) payload = {"video_settings": {"clip_length_max": value}} self._ring.query(url, method="PATCH", json=payload) self._ring.update_devices() @property def connection_status(self) -> str | None: """Return connection status.""" if self.kind in INTERCOM_KINDS: return self._attrs.get("alerts", {}).get("connection") return None @property def allowed_users(self) -> list[dict[str, Any]] | None: """Return list of users allowed or invited to access.""" if self.kind in INTERCOM_KINDS: url = INTERCOM_ALLOWED_USERS.format(self.location_id) return self._ring.query(url, method="GET").json() return None def open_door(self, user_id: int = -1) -> bool: """Open the door.""" if self.kind in INTERCOM_KINDS: url = INTERCOM_OPEN_ENDPOINT.format(self.device_api_id) request_id = str(uuid.uuid4()) # params can also accept: # issue_time: in seconds # command_timeout: in seconds payload = { "command_name": "device_rpc", "request": { "id": request_id, "jsonrpc": "2.0", "method": "unlock_door", "params": { "door_id": 0, "user_id": user_id, }, }, } response = self._ring.query(url, method="PUT", json=payload).json() self._ring.update_devices() if response.get("result", {}).get("code", -1) == 0: return True return False def invite_access(self, email: str) -> bool: """Invite user.""" if self.kind in INTERCOM_KINDS: url = INTERCOM_INVITATIONS_ENDPOINT.format(self.location_id) payload = { "invitation": { "doorbot_ids": [self.device_api_id], "invited_email": email, "group_ids": [], } } self._ring.query(url, method="POST", json=payload) return True return False def remove_access(self, user_id: int) -> bool: """Remove user access or invitation.""" if self.kind in INTERCOM_KINDS: url = INTERCOM_INVITATIONS_DELETE_ENDPOINT.format(self.location_id, user_id) self._ring.query(url, method="DELETE") return True return False python-ring-doorbell-0.8.12/ring_doorbell/py.typed000066400000000000000000000000001463726264600222060ustar00rootroot00000000000000python-ring-doorbell-0.8.12/ring_doorbell/ring.py000066400000000000000000000354041463726264600220400ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Doorbell module.""" from __future__ import annotations import logging from itertools import chain from time import time from typing import TYPE_CHECKING, Any, Iterator, Mapping, Sequence from ring_doorbell import RingEvent from ring_doorbell.chime import RingChime from ring_doorbell.doorbot import RingDoorBell from ring_doorbell.exceptions import RingError from ring_doorbell.group import RingLightGroup from ring_doorbell.other import RingOther from ring_doorbell.stickup_cam import RingStickUpCam from .const import ( API_URI, API_VERSION, DEVICES_ENDPOINT, DINGS_ENDPOINT, GROUPS_ENDPOINT, INTERCOM_KINDS, NEW_SESSION_ENDPOINT, ) if TYPE_CHECKING: from requests import Response from ring_doorbell.auth import Auth from ring_doorbell.generic import RingGeneric _logger = logging.getLogger(__name__) class Ring: """A Python Abstraction object to Ring Door Bell.""" def __init__(self, auth: Auth) -> None: """Initialize the Ring object.""" self.auth: Auth = auth self.session = None self.subscription = None self.devices_data: dict[str, dict[int, dict[str, Any]]] = {} self.chime_health_data = None self.doorbell_health_data = None self.dings_data: dict[Any, Any] = {} self.push_dings_data: list[RingEvent] = [] self.groups_data: dict[str, dict[str, Any]] = {} self.init_loop = None def update_data(self) -> None: """Update all data.""" self._update_data() def _update_data(self) -> None: if self.session is None: self.create_session() self.update_devices() self.update_dings() self.update_groups() def _add_event_to_dings_data(self, ring_event: RingEvent) -> None: # Purge expired push_dings now = time() self.push_dings_data = [ re for re in self.push_dings_data if now < re.now + re.expires_in ] self.push_dings_data.append(ring_event) def create_session(self) -> None: """Create a new Ring session.""" session_post_data = { "device": { "hardware_id": self.auth.get_hardware_id(), "metadata": { "api_version": API_VERSION, "device_model": self.auth.get_device_model(), }, "os": "android", } } self.session = self._query( NEW_SESSION_ENDPOINT, method="POST", json=session_post_data, ).json() def update_devices(self) -> None: """Update device data.""" if self.session is None: self.create_session() data: dict[Any, Any] = self._query(DEVICES_ENDPOINT).json() # Index data by device ID. self.devices_data = { device_type: {obj["id"]: obj for obj in devices} for device_type, devices in data.items() } def update_dings(self) -> None: """Update dings data.""" if self.session is None: self.create_session() self.dings_data = self._query(DINGS_ENDPOINT).json() def update_groups(self) -> None: """Update groups data.""" if self.session is None: self.create_session() # Get all locations locations = set() devices = self.devices() for device_type in devices: for dev in devices[device_type]: if dev.location_id is not None: locations.add(dev.location_id) # Query for groups self.groups_data = {} for location in locations: data = self._query(GROUPS_ENDPOINT.format(location)).json() if data["device_groups"]: for group in data["device_groups"]: self.groups_data[group["device_group_id"]] = group def query( # noqa: PLR0913 self, url: str, method: str = "GET", extra_params: dict[str, Any] | None = None, data: bytes | None = None, json: dict[Any, Any] | None = None, timeout: float | None = None, ) -> Response: """Query data from Ring API.""" if self.session is None: self.create_session() return self._query(url, method, extra_params, data, json, timeout) def _query( # noqa: PLR0913 self, url: str, method: str = "GET", extra_params: dict[str, Any] | None = None, data: bytes | None = None, json: dict[Any, Any] | None = None, timeout: float | None = None, ) -> Response: _logger.debug( "url: %s\nmethod: %s\njson: %s\ndata: %s\n extra_params: %s", url, method, json, data, extra_params, ) response = self.auth.query( API_URI + url, method=method, extra_params=extra_params, data=data, json=json, timeout=timeout, ) _logger.debug("response_text %s", response.text) return response def devices(self) -> RingDevices: """Get all devices.""" return RingDevices(self, self.devices_data) def get_device_list(self) -> Sequence[RingGeneric]: """Get a combined list of all devices.""" devices = self.devices() return list( chain( devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"], devices["chimes"], devices["other"], ) ) def get_device_by_name(self, device_name: str) -> RingGeneric | None: """Return a device using it's name.""" all_devices = self.get_device_list() names_to_idx = {device.name: idx for (idx, device) in enumerate(all_devices)} return ( None if device_name not in names_to_idx else all_devices[names_to_idx[device_name]] ) def get_video_device_by_name(self, device_name: str) -> RingDoorBell | None: """Return a device using it's name.""" video_devices = self.video_devices() names_to_idx = {device.name: idx for (idx, device) in enumerate(video_devices)} return ( None if device_name not in names_to_idx else video_devices[names_to_idx[device_name]] ) def get_device_by_api_id(self, device_api_id: int) -> RingGeneric | None: """Return a device using it's id.""" all_devices = self.get_device_list() api_id_to_idx = { device.device_api_id: idx for (idx, device) in enumerate(all_devices) } return ( None if device_api_id not in api_id_to_idx else all_devices[api_id_to_idx[device_api_id]] ) def video_devices(self) -> Sequence[RingDoorBell]: """Get all devices.""" devices = self.devices() return list( chain(devices.doorbots, devices.authorized_doorbots, devices.stickup_cams) ) def groups(self) -> Mapping[str, RingLightGroup]: """Get all groups.""" groups = {} for group_id in self.groups_data: groups[group_id] = RingLightGroup(self, group_id) return groups def active_alerts(self) -> Sequence[RingEvent]: """Get active alerts.""" now = time() # Purge expired push_dings self.push_dings_data = [ re for re in self.push_dings_data if now < re.now + re.expires_in ] # Get unique id dictionary alerts: dict[tuple[int, int, str], RingEvent] = {} for re in self.push_dings_data: key = (re.doorbot_id, re.id, re.kind) if key not in alerts or re.now > alerts[key].now: alerts[key] = re for ding_data in self.dings_data: expires_at = ding_data.get("now") + ding_data.get("expires_in") if now < expires_at: re = RingEvent( id=ding_data["id"], doorbot_id=ding_data["doorbot_id"], device_name=ding_data["doorbot_description"], device_kind=ding_data["device_kind"], now=ding_data["now"], expires_in=ding_data["expires_in"], kind=ding_data["kind"], state=ding_data["state"], ) key = (re.doorbot_id, re.id, re.kind) if key not in alerts or re.now > alerts[key].now: alerts[key] = re return list(alerts.values()) class RingDevices: """Class to represent collection of devices.""" def __init__( self, ring: Ring, devices_data: dict[str, dict[int, dict[str, Any]]] ) -> None: """Initialise the devices from the api response.""" self._stickup_cams: list[RingStickUpCam] = [] self._chimes: list[RingChime] = [] self._doorbots: list[RingDoorBell] = [] self._authorized_doorbots: list[RingDoorBell] = [] self._other: list[RingOther] = [] for device_type, devices in devices_data.items(): if device_type == "stickup_cams": self._stickup_cams = [ RingStickUpCam(ring, device_id) for device_id in devices ] if device_type == "chimes": self._chimes = [RingChime(ring, device_id) for device_id in devices] if device_type == "doorbots": self._doorbots = [ RingDoorBell(ring, device_id) for device_id in devices ] if device_type == "authorized_doorbots": self._authorized_doorbots = [ RingDoorBell(ring, device_id, shared=True) for device_id in devices ] if device_type == "other": self._other = [ RingOther(ring, device_id, shared=True) for device_id, device in devices.items() if (device_kind := device.get("kind")) and device_kind in INTERCOM_KINDS ] self._all_devices = { device.id: device for device in chain( self._stickup_cams, self._chimes, self._doorbots, self._authorized_doorbots, self._other, ) } def __getitem__(self, device_type: str) -> Sequence[RingGeneric]: """Get a generic device by type.""" if device_type == "stickup_cams": return self._stickup_cams if device_type == "chimes": return self._chimes if device_type == "doorbots": return self._doorbots if device_type == "authorized_doorbots": return self._authorized_doorbots if device_type == "other": return self._other msg = f"Invalid device_type {device_type}" raise RingError(msg) def __iter__(self) -> Iterator[str]: """Device type iterator.""" return iter( ["stickup_cams", "chimes", "doorbots", "authorized_doorbots", "other"] ) @property def stickup_cams(self) -> Sequence[RingStickUpCam]: """The stickup cams.""" return self._stickup_cams @property def chimes(self) -> Sequence[RingChime]: """The chimes.""" return self._chimes @property def doorbots(self) -> Sequence[RingDoorBell]: """The doorbots.""" return self._doorbots @property def authorized_doorbots(self) -> Sequence[RingDoorBell]: """The authorized_doorbots.""" return self._authorized_doorbots @property def doorbells(self) -> Sequence[RingDoorBell]: """The doorbells, i.e. doorbots and authorized_doorbots combined.""" return self._doorbots + self._authorized_doorbots @property def other(self) -> Sequence[RingOther]: """The other devices, i.e. intercoms.""" return self._other @property def all_devices(self) -> Sequence[RingGeneric]: """All devices combined.""" return list(self._all_devices.values()) @property def video_devices(self) -> Sequence[RingDoorBell]: """The video devices, i.e. doorbells and stickup_cams.""" return [*self._doorbots, *self._authorized_doorbots, *self._stickup_cams] def get_device(self, device_api_id: int) -> RingGeneric: """Get device by api id.""" if device := self._all_devices.get(device_api_id): return device msg = f"device with id {device_api_id} not found" raise RingError(msg) def get_doorbell(self, device_api_id: int) -> RingDoorBell: """Get doorbell by api id.""" if ( (device := self._all_devices.get(device_api_id)) and isinstance(device, RingDoorBell) and not issubclass(device.__class__, RingDoorBell) ): return device msg = f"doorbell with id {device_api_id} not found" raise RingError(msg) def get_stickup_cam(self, device_api_id: int) -> RingStickUpCam: """Get stickup_cam by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingStickUpCam ): return device msg = f"stickup_cam with id {device_api_id} not found" raise RingError(msg) def get_chime(self, device_api_id: int) -> RingChime: """Get chime by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingChime ): return device msg = f"chime with id {device_api_id} not found" raise RingError(msg) def get_other(self, device_api_id: int) -> RingOther: """Get other device by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingOther ): return device msg = f"other device with id {device_api_id} not found" raise RingError(msg) def get_video_device(self, device_api_id: int) -> RingDoorBell: """Get video capable device by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingDoorBell ): return device msg = f"video capable device with id {device_api_id} not found" raise RingError(msg) def __str__(self) -> str: """Get string representation of devices.""" d = {dev_type: self.__getitem__(dev_type) for dev_type in self.__iter__()} return "{" + "\n".join(f"{k!r}: {v!r}," for k, v in d.items()) + "}" def __repr__(self) -> str: """Return repr of devices.""" d = {dev_type: self.__getitem__(dev_type) for dev_type in self.__iter__()} return repr(d) python-ring-doorbell-0.8.12/ring_doorbell/stickup_cam.py000066400000000000000000000141531463726264600234010ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Doorbell wrapper.""" from __future__ import annotations import logging from ring_doorbell.const import ( FLOODLIGHT_CAM_KINDS, FLOODLIGHT_CAM_PLUS_KINDS, FLOODLIGHT_CAM_PRO_KINDS, INDOOR_CAM_GEN2_KINDS, INDOOR_CAM_KINDS, LIGHTS_ENDPOINT, MSG_ALLOWED_VALUES, MSG_VOL_OUTBOUND, SIREN_DURATION_MAX, SIREN_DURATION_MIN, SIREN_ENDPOINT, SPOTLIGHT_CAM_BATTERY_KINDS, SPOTLIGHT_CAM_PLUS_KINDS, SPOTLIGHT_CAM_PRO_KINDS, SPOTLIGHT_CAM_WIRED_KINDS, STICKUP_CAM_BATTERY_KINDS, STICKUP_CAM_ELITE_KINDS, STICKUP_CAM_GEN3_KINDS, STICKUP_CAM_KINDS, RingCapability, ) from ring_doorbell.doorbot import RingDoorBell from ring_doorbell.exceptions import RingError _LOGGER = logging.getLogger(__name__) class RingStickUpCam(RingDoorBell): """Implementation for RingStickUpCam.""" @property def family(self) -> str: """Return Ring device family type.""" return "stickup_cams" @property def model(self) -> str: # noqa: C901, PLR0911, PLR0912 """Return Ring device model name.""" if self.kind in FLOODLIGHT_CAM_KINDS: return "Floodlight Cam" if self.kind in FLOODLIGHT_CAM_PRO_KINDS: return "Floodlight Cam Pro" if self.kind in FLOODLIGHT_CAM_PLUS_KINDS: return "Floodlight Cam Plus" if self.kind in INDOOR_CAM_KINDS: return "Indoor Cam" if self.kind in INDOOR_CAM_GEN2_KINDS: return "Indoor Cam (2nd Gen)" if self.kind in SPOTLIGHT_CAM_BATTERY_KINDS: return "Spotlight Cam {}".format( self._attrs.get("ring_cam_setup_flow", "battery").title() ) if self.kind in SPOTLIGHT_CAM_WIRED_KINDS: return "Spotlight Cam {}".format( self._attrs.get("ring_cam_setup_flow", "wired").title() ) if self.kind in SPOTLIGHT_CAM_PLUS_KINDS: return "Spotlight Cam Plus" if self.kind in SPOTLIGHT_CAM_PRO_KINDS: return "Spotlight Cam Pro" if self.kind in STICKUP_CAM_KINDS: return "Stick Up Cam" if self.kind in STICKUP_CAM_BATTERY_KINDS: return "Stick Up Cam Battery" if self.kind in STICKUP_CAM_ELITE_KINDS: return "Stick Up Cam Wired" if self.kind in STICKUP_CAM_GEN3_KINDS: return "Stick Up Cam (3rd Gen)" _LOGGER.error("Unknown kind: %s", self.kind) return "Unknown Stickup Cam" def has_capability(self, capability: RingCapability | str) -> bool: """Return if device has specific capability.""" capability = ( capability if isinstance(capability, RingCapability) else RingCapability.from_name(capability) ) if capability == RingCapability.HISTORY: return True if capability == RingCapability.BATTERY: return self.kind in ( SPOTLIGHT_CAM_BATTERY_KINDS + STICKUP_CAM_KINDS + STICKUP_CAM_BATTERY_KINDS + STICKUP_CAM_GEN3_KINDS ) if capability == RingCapability.LIGHT: return self.kind in ( FLOODLIGHT_CAM_KINDS + FLOODLIGHT_CAM_PRO_KINDS + FLOODLIGHT_CAM_PLUS_KINDS + SPOTLIGHT_CAM_BATTERY_KINDS + SPOTLIGHT_CAM_WIRED_KINDS + SPOTLIGHT_CAM_PLUS_KINDS + SPOTLIGHT_CAM_PRO_KINDS ) if capability == RingCapability.SIREN: return self.kind in ( FLOODLIGHT_CAM_KINDS + FLOODLIGHT_CAM_PRO_KINDS + FLOODLIGHT_CAM_PLUS_KINDS + INDOOR_CAM_KINDS + INDOOR_CAM_GEN2_KINDS + SPOTLIGHT_CAM_BATTERY_KINDS + SPOTLIGHT_CAM_WIRED_KINDS + SPOTLIGHT_CAM_PLUS_KINDS + SPOTLIGHT_CAM_PRO_KINDS + STICKUP_CAM_BATTERY_KINDS + STICKUP_CAM_ELITE_KINDS + STICKUP_CAM_GEN3_KINDS ) if capability in [RingCapability.MOTION_DETECTION, RingCapability.VIDEO]: return self.kind in ( FLOODLIGHT_CAM_KINDS + FLOODLIGHT_CAM_PRO_KINDS + FLOODLIGHT_CAM_PLUS_KINDS + INDOOR_CAM_KINDS + INDOOR_CAM_GEN2_KINDS + SPOTLIGHT_CAM_BATTERY_KINDS + SPOTLIGHT_CAM_WIRED_KINDS + SPOTLIGHT_CAM_PLUS_KINDS + SPOTLIGHT_CAM_PRO_KINDS + STICKUP_CAM_KINDS + STICKUP_CAM_BATTERY_KINDS + STICKUP_CAM_ELITE_KINDS + STICKUP_CAM_GEN3_KINDS ) return False @property def lights(self) -> str: """Return lights status.""" return self._attrs.get("led_status", "") @lights.setter def lights(self, state: str) -> None: """Control the lights.""" values = ["on", "off"] if state not in values: raise RingError(MSG_ALLOWED_VALUES.format(", ".join(values))) url = LIGHTS_ENDPOINT.format(self.device_api_id, state) self._ring.query(url, method="PUT") self._ring.update_devices() @property def siren(self) -> int: """Return siren status.""" if siren_status := self._attrs.get("siren_status"): return siren_status.get("seconds_remaining", 0) return 0 @siren.setter def siren(self, duration: int) -> None: """Control the siren.""" if not ( (isinstance(duration, int)) and (SIREN_DURATION_MIN <= duration <= SIREN_DURATION_MAX) ): raise RingError( MSG_VOL_OUTBOUND.format(SIREN_DURATION_MIN, SIREN_DURATION_MAX) ) if duration > 0: state = "on" params = {"duration": duration} else: state = "off" params = {} url = SIREN_ENDPOINT.format(self.device_api_id, state) self._ring.query(url, extra_params=params, method="PUT") self._ring.update_devices() python-ring-doorbell-0.8.12/test.py000066400000000000000000000023441463726264600172340ustar00rootroot00000000000000"""Test module which runs the first example in the README.""" import getpass import json from pathlib import Path from ring_doorbell import Auth, AuthenticationError, Requires2FAError, Ring user_agent = "YourProjectName-1.0" # Change this cache_file = Path(user_agent + ".token.cache") def token_updated(token) -> None: cache_file.write_text(json.dumps(token)) def otp_callback(): return input("2FA code: ") def do_auth(): username = input("Username: ") password = getpass.getpass("Password: ") auth = Auth(user_agent, None, token_updated) try: auth.fetch_token(username, password) except Requires2FAError: auth.fetch_token(username, password, otp_callback()) return auth def main() -> None: if cache_file.is_file(): # auth token is cached auth = Auth(user_agent, json.loads(cache_file.read_text()), token_updated) ring = Ring(auth) try: ring.create_session() # auth token still valid except AuthenticationError: # auth token has expired auth = do_auth() else: auth = do_auth() # Get new auth token ring = Ring(auth) ring.update_data() print(ring.devices()) if __name__ == "__main__": main() python-ring-doorbell-0.8.12/tests/000077500000000000000000000000001463726264600170425ustar00rootroot00000000000000python-ring-doorbell-0.8.12/tests/__init__.py000066400000000000000000000000531463726264600211510ustar00rootroot00000000000000"""Tests for Ring Door Bell components.""" python-ring-doorbell-0.8.12/tests/conftest.py000066400000000000000000000130251463726264600212420ustar00rootroot00000000000000"""Test configuration for the Ring platform.""" import json import re from pathlib import Path from time import time import pytest import requests_mock from ring_doorbell import Auth, Ring from ring_doorbell.const import USER_AGENT from ring_doorbell.listen import can_listen def pytest_configure(config): config.addinivalue_line( "markers", "nolistenmock: mark test to not want the autouse listenmock" ) @pytest.fixture() def auth(requests_mock): """Return auth object.""" auth = Auth(USER_AGENT) auth.fetch_token("foo", "bar") return auth @pytest.fixture() def ring(auth): """Return updated ring object.""" ring = Ring(auth) ring.update_data() return ring def _set_dings_to_now(active_dings) -> None: dings = json.loads(active_dings) for ding in dings: ding["now"] = time() return json.dumps(dings) def load_fixture(filename): """Load a fixture.""" path = Path(Path(__file__).parent / "fixtures" / filename) with path.open() as fdp: return fdp.read() @pytest.fixture(autouse=True) def _listen_mock(mocker, request) -> None: if not can_listen or "nolistenmock" in request.keywords: return mocker.patch("firebase_messaging.FcmPushClient.checkin", return_value="foobar") mocker.patch("firebase_messaging.FcmPushClient.start") mocker.patch("firebase_messaging.FcmPushClient.is_started", return_value=True) # setting the fixture name to requests_mock allows other # tests to pull in request_mock and append uris @pytest.fixture(autouse=True, name="requests_mock") def requests_mock_fixture(): with requests_mock.Mocker() as mock: mock.post( "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json") ) mock.post( "https://api.ring.com/clients_api/session", text=load_fixture("ring_session.json"), ) mock.get( "https://api.ring.com/clients_api/ring_devices", text=load_fixture("ring_devices.json"), ) mock.get( re.compile(r"https:\/\/api\.ring\.com\/clients_api\/chimes\/\d+\/health"), text=load_fixture("ring_chime_health_attrs.json"), ) mock.get( re.compile(r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/health"), text=load_fixture("ring_doorboot_health_attrs.json"), ) mock.get( "https://api.ring.com/clients_api/doorbots/987652/history", text=load_fixture("ring_doorbot_history.json"), ) mock.get( "https://api.ring.com/clients_api/doorbots/185036587/history", text=load_fixture("ring_intercom_history.json"), ) mock.get( "https://api.ring.com/clients_api/dings/active", text=_set_dings_to_now(load_fixture("ring_ding_active.json")), ) mock.put( "https://api.ring.com/clients_api/doorbots/987652/floodlight_light_off", text="ok", ) mock.put( "https://api.ring.com/clients_api/doorbots/987652/floodlight_light_on", text="ok", ) mock.put("https://api.ring.com/clients_api/doorbots/987652/siren_on", text="ok") mock.put( "https://api.ring.com/clients_api/doorbots/987652/siren_off", text="ok" ) mock.get( "https://api.ring.com/groups/v1/locations/mock-location-id/groups", text=load_fixture("ring_groups.json"), ) mock.get( "https://api.ring.com/groups/v1/locations/" "mock-location-id/groups/mock-group-id/devices", text=load_fixture("ring_group_devices.json"), ) mock.post( "https://api.ring.com/groups/v1/locations/" "mock-location-id/groups/mock-group-id/devices", text="ok", ) mock.patch( re.compile( r"https:\/\/api\.ring\.com\/devices\/v1\/devices\/\d+\/settings" ), text="ok", ) mock.get( re.compile(r"https:\/\/api\.ring\.com\/clients_api\/dings\/\d+\/recording"), status_code=200, content=b"123456", ) mock.get( "https://api.ring.com/clients_api/dings/9876543212/recording", status_code=200, content=b"123456", ) mock.patch( "https://api.ring.com/clients_api/device", status_code=204, content=b"", ) mock.put( "https://api.ring.com/clients_api/doorbots/185036587", status_code=204, content=b"", ) mock.get( "https://api.ring.com/devices/v1/devices/185036587/settings", text=load_fixture("ring_intercom_settings.json"), ) mock.get( "https://api.ring.com/clients_api/locations/mock-location-id/users", text=load_fixture("ring_intercom_users.json"), ) mock.post( "https://api.ring.com/clients_api/locations/mock-location-id/invitations", text="ok", ) mock.delete( ( "https://api.ring.com/clients_api/locations/" "mock-location-id/invitations/123456789" ), text="ok", ) requestid = "44529542-3ed7-41da-807e-c170a01bac1d" mock.put( "https://api.ring.com/commands/v1/devices/185036587/device_rpc", text='{"result": {"code": 0}, "id": "' + requestid + '", "jsonrpc": "2.0"}', ) yield mock python-ring-doorbell-0.8.12/tests/fixtures/000077500000000000000000000000001463726264600207135ustar00rootroot00000000000000python-ring-doorbell-0.8.12/tests/fixtures/ring_chime_health_attrs.json000066400000000000000000000010661463726264600264570ustar00rootroot00000000000000{ "device_health": { "average_signal_category": "good", "average_signal_strength": -39, "battery_percentage": 100, "battery_percentage_category": null, "battery_voltage": null, "battery_voltage_category": null, "firmware": "1.2.3", "firmware_out_of_date": false, "id": 999999, "latest_signal_category": "good", "latest_signal_strength": -39, "updated_at": "2017-09-30T07:05:03Z", "wifi_is_ring_network": false, "wifi_name": "ring_mock_wifi" } } python-ring-doorbell-0.8.12/tests/fixtures/ring_devices.json000066400000000000000000000404631463726264600242560ustar00rootroot00000000000000{ "authorized_doorbots": [ { "address": "123 Second St", "alerts": {"connection": "online"}, "battery_life": 51, "description": "Back Door", "device_id": "aacdef124", "external_connection": false, "features": { "advanced_motion_enabled": false, "motion_message_enabled": false, "motions_enabled": true, "people_only_enabled": false, "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", "id": 987653, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, "motion_snooze": null, "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Foo", "id": 999999, "last_name": "Bar"}, "settings": { "chime_settings": { "duration": 3, "enable": true, "type": 1}, "doorbell_volume": 5, "enable_vod": true, "live_view_preset_profile": "highest", "live_view_presets": [ "low", "middle", "high", "highest"], "motion_detection_enabled": false, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": [ "none", "low", "medium", "high"]}, "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York"}], "chimes": [ { "address": "123 Main St", "alerts": {"connection": "online"}, "description": "Downstairs", "device_id": "abcdef123", "do_not_disturb": {"seconds_left": 0}, "features": {"ringtones_enabled": true}, "firmware_version": "1.2.3", "id": 999999, "kind": "chime", "latitude": 12.000000, "longitude": -70.12345, "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Marcelo", "id": 999999, "last_name": "Assistant"}, "settings": { "ding_audio_id": null, "ding_audio_user_id": null, "motion_audio_id": null, "motion_audio_user_id": null, "volume": 2}, "time_zone": "America/New_York"}], "doorbots": [ { "address": "123 Main St", "alerts": {"connection": "online"}, "battery_life": 4081, "description": "Front Door", "device_id": "aacdef123", "external_connection": false, "features": { "advanced_motion_enabled": false, "motion_message_enabled": false, "motions_enabled": true, "people_only_enabled": false, "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", "id": 987652, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, "motion_snooze": null, "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Home", "id": 999999, "last_name": "Assistant"}, "settings": { "chime_settings": { "duration": 3, "enable": true, "type": 0}, "doorbell_volume": 1, "enable_vod": true, "live_view_preset_profile": "highest", "live_view_presets": [ "low", "middle", "high", "highest"], "motion_detection_enabled": true, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": [ "null", "low", "medium", "high"]}, "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York"}], "stickup_cams": [ { "address": "123 Main St", "alerts": {"connection": "online"}, "battery_life": 100, "description": "Front", "device_id": "aacdef123", "external_connection": false, "features": { "advanced_motion_enabled": false, "motion_message_enabled": false, "motions_enabled": true, "night_vision_enabled": false, "people_only_enabled": false, "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", "id": 987652, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "off", "location_id": "mock-location-id", "longitude": -70.12345, "motion_snooze": {"scheduled": true}, "night_mode_status": "false", "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Foo", "id": 999999, "last_name": "Bar"}, "ring_cam_light_installed": "false", "ring_id": null, "settings": { "chime_settings": { "duration": 10, "enable": true, "type": 0}, "doorbell_volume": 11, "enable_vod": true, "floodlight_settings": { "duration": 30, "priority": 0}, "light_schedule_settings": { "end_hour": 0, "end_minute": 0, "start_hour": 0, "start_minute": 0}, "live_view_preset_profile": "highest", "live_view_presets": [ "low", "middle", "high", "highest"], "motion_detection_enabled": false, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": [ "none", "low", "medium", "high"], "motion_zones": { "active_motion_filter": 1, "advanced_object_settings": { "human_detection_confidence": { "day": 0.7, "night": 0.7}, "motion_zone_overlap": { "day": 0.1, "night": 0.2}, "object_size_maximum": { "day": 0.8, "night": 0.8}, "object_size_minimum": { "day": 0.03, "night": 0.05}, "object_time_overlap": { "day": 0.1, "night": 0.6} }, "enable_audio": false, "pir_settings": { "sensitivity1": 1, "sensitivity2": 1, "sensitivity3": 1, "zone_mask": 6}, "sensitivity": 5, "zone1": { "name": "Zone 1", "state": 2, "vertex1": {"x": 0.0, "y": 0.0}, "vertex2": {"x": 0.0, "y": 0.0}, "vertex3": {"x": 0.0, "y": 0.0}, "vertex4": {"x": 0.0, "y": 0.0}, "vertex5": {"x": 0.0, "y": 0.0}, "vertex6": {"x": 0.0, "y": 0.0}, "vertex7": {"x": 0.0, "y": 0.0}, "vertex8": {"x": 0.0, "y": 0.0}}, "zone2": { "name": "Zone 2", "state": 2, "vertex1": {"x": 0.0, "y": 0.0}, "vertex2": {"x": 0.0, "y": 0.0}, "vertex3": {"x": 0.0, "y": 0.0}, "vertex4": {"x": 0.0, "y": 0.0}, "vertex5": {"x": 0.0, "y": 0.0}, "vertex6": {"x": 0.0, "y": 0.0}, "vertex7": {"x": 0.0, "y": 0.0}, "vertex8": {"x": 0.0, "y": 0.0}}, "zone3": { "name": "Zone 3", "state": 2, "vertex1": {"x": 0.0, "y": 0.0}, "vertex2": {"x": 0.0, "y": 0.0}, "vertex3": {"x": 0.0, "y": 0.0}, "vertex4": {"x": 0.0, "y": 0.0}, "vertex5": {"x": 0.0, "y": 0.0}, "vertex6": {"x": 0.0, "y": 0.0}, "vertex7": {"x": 0.0, "y": 0.0}, "vertex8": {"x": 0.0, "y": 0.0}}}, "pir_motion_zones": [0, 1, 1], "pir_settings": { "sensitivity1": 1, "sensitivity2": 1, "sensitivity3": 1, "zone_mask": 6}, "stream_setting": 0, "video_settings": { "ae_level": 0, "birton": null, "brightness": 0, "contrast": 64, "saturation": 80}}, "siren_status": {"seconds_remaining": 0}, "stolen": false, "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York" }], "other": [ { "id": 185036587, "kind": "intercom_handset_audio", "description": "Ingress", "location_id": "mock-location-id", "schema_id": null, "is_sidewalk_gateway": false, "created_at": "2023-12-01T18:05:25Z", "deactivated_at": null, "owner": { "id": 762490876, "first_name": "", "last_name": "", "email": "" }, "device_id": "124ba1b3fe1a", "time_zone": "Europe/Rome", "firmware_version": "Up to Date", "owned": true, "ring_net_id": null, "settings": { "features_confirmed": 5, "show_recordings": true, "recording_ttl": 180, "recording_enabled": false, "keep_alive": null, "keep_alive_auto": 45.0, "doorbell_volume": 8, "enable_chime": 1, "theft_alarm_enable": 0, "use_cached_domain": 1, "use_server_ip": 0, "server_domain": "fw.ring.com", "server_ip": null, "enable_log": 1, "forced_keep_alive": null, "mic_volume": 11, "chime_settings": { "enable": true, "type": 2, "duration": 10 }, "intercom_settings": { "ring_to_open": false, "predecessor": "{\"make\":\"Comelit\",\"model\":\"2738W\",\"wires\":2}", "config": "{\"intercom_type\": 2, \"number_of_wires\": 2, \"autounlock_enabled\": false, \"speaker_gain\": [-49, -35, -25, -21, -16, -9, -6, -3, 0, 3, 6, 9], \"digital\": {\"audio_amp\": 0, \"chg_en\": false, \"fast_chg\": false, \"bypass\": false, \"idle_lvl\": 32, \"ext_audio\": false, \"ext_audio_term\": 0, \"off_hk_tm\": 0, \"unlk_ka\": false, \"unlock\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"ring\": {\"cap_tm\": 40, \"rpl_tm\": 200, \"gain\": 2000, \"cmp_thr\": 4500, \"lvl\": 28000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"m\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_off\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_on\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}}}", "intercom_type": "DF", "replication": 1, "unlock_mode": 0 }, "voice_volume": 11 }, "alerts": { "connection": "online", "ota_status": "timeout" }, "function": { "name": null }, "subscribed": false, "battery_life": "52", "features": { "cfes_eligible": false, "motion_zone_recommendation": false, "motions_enabled": true, "show_recordings": true, "show_vod_settings": true, "rich_notifications_eligible": false, "show_offline_motion_events": false, "sheila_camera_eligible": null, "sheila_camera_processing_eligible": null, "dynamic_network_switching_eligible": false, "chime_auto_detect_capable": false, "missing_key_delivery_address": false, "show_24x7_lite": false, "recording_24x7_eligible": null }, "metadata": { "ethernet": false, "legacy_fw_migrated": true, "imported_from_amazon": false, "is_sidewalk_gateway": false, "key_access_point_associated": true } }, { "id": 99999999, "kind": "three_p_cam", "description": "Third party Cam", "location_id": "**REDACTED**", "schema_id": null, "is_sidewalk_gateway": true, "deactivated_at": null, "hardware_id": "ABCD12345", "time_zone": "America/Phoenix", "stolen": false, "owned": true, "settings": { "enable_vod": 0, "powered_on": null, "supported_capabilities": {}, "camera_stream_configurations": [] }, "features": { "cfes_eligible": false, "motion_zone_recommendation": false, "motions_enabled": true, "show_recordings": false, "show_vod_settings": true, "show_offline_motion_events": false }, "owner": { "id": "**REDACTED**", "first_name": "", "last_name": "", "email": "" }, "ring_net_id": "**REDACTED**", "third_party_manufacturer": "amazon1p", "third_party_model": "A3RMGO6LYLH7YN", "third_party_dsn": "ABCD12345", "third_party_tags": [], "metadata": { "third_party_manufacturer": "amazon1p", "third_party_model": "A3RMGO6LYLH7YN", "is_sidewalk_gateway": true, "third_party_dsn": "ABCD12345" }, "alerts": { "connection": "online" } } ] }python-ring-doorbell-0.8.12/tests/fixtures/ring_devices_updated.json000066400000000000000000000243441463726264600257640ustar00rootroot00000000000000{ "authorized_doorbots": [ { "address": "123 Second St", "alerts": {"connection": "online"}, "battery_life": 51, "description": "Back Door", "device_id": "aacdef124", "external_connection": false, "features": { "advanced_motion_enabled": false, "motion_message_enabled": false, "motions_enabled": true, "people_only_enabled": false, "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", "id": 987653, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, "motion_snooze": null, "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Foo", "id": 999999, "last_name": "Bar"}, "settings": { "chime_settings": { "duration": 3, "enable": true, "type": 1}, "doorbell_volume": 5, "enable_vod": true, "live_view_preset_profile": "highest", "live_view_presets": [ "low", "middle", "high", "highest"], "motion_detection_enabled": true, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": [ "none", "low", "medium", "high"]}, "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York"}], "chimes": [ { "address": "123 Main St", "alerts": {"connection": "online"}, "description": "Downstairs", "device_id": "abcdef123", "do_not_disturb": {"seconds_left": 0}, "features": {"ringtones_enabled": true}, "firmware_version": "1.2.3", "id": 999999, "kind": "chime", "latitude": 12.000000, "longitude": -70.12345, "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Marcelo", "id": 999999, "last_name": "Assistant"}, "settings": { "ding_audio_id": null, "ding_audio_user_id": null, "motion_audio_id": null, "motion_audio_user_id": null, "volume": 2}, "time_zone": "America/New_York"}], "doorbots": [ { "address": "123 Main St", "alerts": {"connection": "online"}, "battery_life": 4081, "description": "Front Door", "device_id": "aacdef123", "external_connection": false, "features": { "advanced_motion_enabled": false, "motion_message_enabled": false, "motions_enabled": true, "people_only_enabled": false, "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", "id": 987652, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, "motion_snooze": null, "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Home", "id": 999999, "last_name": "Assistant"}, "settings": { "chime_settings": { "duration": 3, "enable": true, "type": 0}, "doorbell_volume": 1, "enable_vod": true, "live_view_preset_profile": "highest", "live_view_presets": [ "low", "middle", "high", "highest"], "motion_detection_enabled": false, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": [ "null", "low", "medium", "high"]}, "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York"}], "stickup_cams": [ { "address": "123 Main St", "alerts": {"connection": "online"}, "battery_life": 100, "description": "Front", "device_id": "aacdef123", "external_connection": false, "features": { "advanced_motion_enabled": false, "motion_message_enabled": false, "motions_enabled": true, "night_vision_enabled": false, "people_only_enabled": false, "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", "id": 987652, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "off", "location_id": "mock-location-id", "longitude": -70.12345, "motion_snooze": {"scheduled": true}, "night_mode_status": "false", "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Foo", "id": 999999, "last_name": "Bar"}, "ring_cam_light_installed": "false", "ring_id": null, "settings": { "chime_settings": { "duration": 10, "enable": true, "type": 0}, "doorbell_volume": 11, "enable_vod": true, "floodlight_settings": { "duration": 30, "priority": 0}, "light_schedule_settings": { "end_hour": 0, "end_minute": 0, "start_hour": 0, "start_minute": 0}, "live_view_preset_profile": "highest", "live_view_presets": [ "low", "middle", "high", "highest"], "motion_detection_enabled": true, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": [ "none", "low", "medium", "high"], "motion_zones": { "active_motion_filter": 1, "advanced_object_settings": { "human_detection_confidence": { "day": 0.7, "night": 0.7}, "motion_zone_overlap": { "day": 0.1, "night": 0.2}, "object_size_maximum": { "day": 0.8, "night": 0.8}, "object_size_minimum": { "day": 0.03, "night": 0.05}, "object_time_overlap": { "day": 0.1, "night": 0.6} }, "enable_audio": false, "pir_settings": { "sensitivity1": 1, "sensitivity2": 1, "sensitivity3": 1, "zone_mask": 6}, "sensitivity": 5, "zone1": { "name": "Zone 1", "state": 2, "vertex1": {"x": 0.0, "y": 0.0}, "vertex2": {"x": 0.0, "y": 0.0}, "vertex3": {"x": 0.0, "y": 0.0}, "vertex4": {"x": 0.0, "y": 0.0}, "vertex5": {"x": 0.0, "y": 0.0}, "vertex6": {"x": 0.0, "y": 0.0}, "vertex7": {"x": 0.0, "y": 0.0}, "vertex8": {"x": 0.0, "y": 0.0}}, "zone2": { "name": "Zone 2", "state": 2, "vertex1": {"x": 0.0, "y": 0.0}, "vertex2": {"x": 0.0, "y": 0.0}, "vertex3": {"x": 0.0, "y": 0.0}, "vertex4": {"x": 0.0, "y": 0.0}, "vertex5": {"x": 0.0, "y": 0.0}, "vertex6": {"x": 0.0, "y": 0.0}, "vertex7": {"x": 0.0, "y": 0.0}, "vertex8": {"x": 0.0, "y": 0.0}}, "zone3": { "name": "Zone 3", "state": 2, "vertex1": {"x": 0.0, "y": 0.0}, "vertex2": {"x": 0.0, "y": 0.0}, "vertex3": {"x": 0.0, "y": 0.0}, "vertex4": {"x": 0.0, "y": 0.0}, "vertex5": {"x": 0.0, "y": 0.0}, "vertex6": {"x": 0.0, "y": 0.0}, "vertex7": {"x": 0.0, "y": 0.0}, "vertex8": {"x": 0.0, "y": 0.0}}}, "pir_motion_zones": [0, 1, 1], "pir_settings": { "sensitivity1": 1, "sensitivity2": 1, "sensitivity3": 1, "zone_mask": 6}, "stream_setting": 0, "video_settings": { "ae_level": 0, "birton": null, "brightness": 0, "contrast": 64, "saturation": 80}}, "siren_status": {"seconds_remaining": 0}, "stolen": false, "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York"}] } python-ring-doorbell-0.8.12/tests/fixtures/ring_ding_active.json000066400000000000000000000046231463726264600251060ustar00rootroot00000000000000[ { "id": 234567890123456, "id_str": "234567890123456", "state": "ringing", "protocol": "sip", "doorbot_id": 123456, "doorbot_description": "Front Floodcam", "device_kind": "floodlight_v2", "motion": false, "snapshot_url": "", "kind": "on_demand_link", "sip_server_ip": "192.168.0.1", "sip_server_port": 8557, "sip_server_tls": true, "sip_session_id": "r.ms.FOO/C+sklfjhweihfkwefnklnew", "sip_from": "sip:1234@ring.com", "sip_to": "sip:r.ms.FOO/C+sklfjhweihfkwefnklnew@192.168.0.1:12345;transport=tls", "audio_jitter_buffer_ms": 300, "video_jitter_buffer_ms": 300, "expires_in": 167, "optimization_level": 3, "now": 1696401245.416, "sip_token": "", "sip_ding_id": "876543121", "ding_encrypted": false, "requested_at": 1696401245416 }, { "id": 1234567890123456, "id_str": "1234567890123456", "state": "ringing", "protocol": "sip", "doorbot_id": 987652, "doorbot_description": "Front Door", "device_kind": "lpd_v1", "motion": false, "snapshot_url": "", "kind": "motion", "sip_server_ip": "192.168.0.1", "sip_server_port": 1234, "sip_server_tls": true, "sip_session_id": "r.ms.KY00wXB0l/hfiwehjod+wnefwaekjf", "sip_from": "sip:12345@ring.com", "sip_to": "sip:r.ms.KY00wXB0l/hfiwehjod+wnefwaekjf@192.168.0.1:12345;transport=tls", "audio_jitter_buffer_ms": 300, "video_jitter_buffer_ms": 300, "expires_in": 167, "optimization_level": 1, "now": 1696401245.416, "sip_token": "", "sip_ding_id": "987654211335", "ding_encrypted": false, "requested_at": 1696401245416 }, { "audio_jitter_buffer_ms": 0, "device_kind": "lpd_v1", "doorbot_description": "Front Door", "doorbot_id": 987652, "expires_in": 180, "id": 123456789, "id_str": "123456789", "kind": "ding", "motion": false, "now": 1490949469.5498993, "optimization_level": 1, "protocol": "sip", "sip_ding_id": "123456789", "sip_endpoints": null, "sip_from": "sip:abc123@ring.com", "sip_server_ip": "192.168.0.1", "sip_server_port": "15063", "sip_server_tls": "false", "sip_session_id": "28qdvjh-2043", "sip_to": "sip:28qdvjh-2043@192.168.0.1:15063;transport=tcp", "sip_token": "adecc24a428ed704b2d80adb621b5775755915529639e", "snapshot_url": "", "state": "ringing", "video_jitter_buffer_ms": 0 } ] python-ring-doorbell-0.8.12/tests/fixtures/ring_doorboot_health_attrs.json000066400000000000000000000010661463726264600272210ustar00rootroot00000000000000{ "device_health": { "average_signal_category": "good", "average_signal_strength": -39, "battery_percentage": 100, "battery_percentage_category": null, "battery_voltage": null, "battery_voltage_category": null, "firmware": "1.9.2", "firmware_out_of_date": false, "id": 987652, "latest_signal_category": "good", "latest_signal_strength": -58, "updated_at": "2017-09-30T07:05:03Z", "wifi_is_ring_network": false, "wifi_name": "ring_mock_wifi" } } python-ring-doorbell-0.8.12/tests/fixtures/ring_doorboot_health_attrs_id987653.json000066400000000000000000000010661463726264600304030ustar00rootroot00000000000000{ "device_health": { "average_signal_category": "good", "average_signal_strength": -39, "battery_percentage": 100, "battery_percentage_category": null, "battery_voltage": null, "battery_voltage_category": null, "firmware": "1.9.2", "firmware_out_of_date": false, "id": 987653, "latest_signal_category": "good", "latest_signal_strength": -58, "updated_at": "2017-09-30T07:05:03Z", "wifi_is_ring_network": false, "wifi_name": "ring_mock_wifi" } } python-ring-doorbell-0.8.12/tests/fixtures/ring_doorbot_history.json000066400000000000000000000012301463726264600260520ustar00rootroot00000000000000[{ "answered": false, "created_at": "2017-03-05T15:03:40.000Z", "events": [], "favorite": false, "id": 987654321, "kind": "motion", "recording": {"status": "ready"}, "snapshot_url": "" }, { "answered": false, "created_at": "2017-03-05T16:03:40.000Z", "events": [], "favorite": false, "id": 9876543212, "kind": "motion", "recording": {"status": "ready"}, "snapshot_url": "" }, { "answered": false, "created_at": "2017-03-05T16:03:40.000Z", "events": [], "favorite": false, "id": 1234567890123456, "kind": "ding", "recording": {"status": "ready"}, "snapshot_url": "" }] python-ring-doorbell-0.8.12/tests/fixtures/ring_group_devices.json000066400000000000000000000013471463726264600254700ustar00rootroot00000000000000{ "device_group_id": "mock-group-id", "motion_snooze_on": false, "devices": [ { "id": "12345678", "lights_on": false, "motion_detection_on": false, "motion_notifications_on": false, "motion_activated_lights": false, "motion_message_on": false, "siren_on": false, "motion_snooze_seconds_left": 0, "motion_light_duration_seconds": 0 } ], "lights_on": false, "motion_detection_on": false, "motion_notifications_on": false, "motion_activated_lights": false, "motion_message_on": false, "siren_on": false, "motion_snooze_seconds_left": 0, "motion_light_duration_seconds": 0 } python-ring-doorbell-0.8.12/tests/fixtures/ring_groups.json000066400000000000000000000014221463726264600241430ustar00rootroot00000000000000{ "device_groups": [ { "device_group_id": "mock-group-id", "location_id": "mock-location-id", "name": "Landscape", "devices": [ { "doorbot_id": 12345678, "location_id": "mock-location-id", "type": "beams_ct200_transformer", "mac_address": null, "hardware_id": "1234567890", "name": "Mock Transformer", "deleted_at": null } ], "created_at": "2020-11-03T22:07:05Z", "updated_at": "2020-11-19T03:52:59Z", "deleted_at": null, "external_id": "12345678-1234-5678-90ab-1234567890ab" } ] } python-ring-doorbell-0.8.12/tests/fixtures/ring_intercom_history.json000066400000000000000000000051471463726264600262350ustar00rootroot00000000000000[ { "id": 7330963245622279024, "created_at": "2024-02-02T11:21:24.000Z", "answered": false, "events": [], "kind": "ding", "favorite": false, "snapshot_url": "", "recording": { "status": "ready" }, "duration": 40.0, "cv_properties": { "person_detected": null, "stream_broken": false, "detection_type": null, "cv_triggers": null, "detection_types": null, "security_alerts": null }, "properties": { "is_alexa": false, "is_sidewalk": false, "is_autoreply": false }, "doorbot": { "id": 185036587, "description": "Ingresso", "type": "intercom_handset_audio" }, "device_placement": null, "geolocation": null, "last_location": null, "siren": null, "is_e2ee": false, "had_subscription": false, "owner_id": "762490876" }, { "id": 7323267080901445808, "created_at": "2024-01-12T17:36:28.000Z", "answered": true, "events": [], "kind": "on_demand", "favorite": false, "snapshot_url": "", "recording": { "status": "ready" }, "duration": 13.0, "cv_properties": { "person_detected": null, "stream_broken": false, "detection_type": null, "cv_triggers": null, "detection_types": null, "security_alerts": null }, "properties": { "is_alexa": false, "is_sidewalk": false, "is_autoreply": false }, "doorbot": { "id": 185036587, "description": "Ingresso", "type": "intercom_handset_audio" }, "device_placement": null, "geolocation": null, "last_location": null, "siren": null, "is_e2ee": false, "had_subscription": false, "owner_id": "762490876" }, { "id": 7307399027047288688, "created_at": "2023-12-01T18:44:28.000Z", "answered": true, "events": [], "kind": "on_demand", "favorite": false, "snapshot_url": "", "recording": { "status": "ready" }, "duration": 43.0, "cv_properties": { "person_detected": null, "stream_broken": false, "detection_type": null, "cv_triggers": null, "detection_types": null, "security_alerts": null }, "properties": { "is_alexa": false, "is_sidewalk": false, "is_autoreply": false }, "doorbot": { "id": 185036587, "description": "Ingresso", "type": "intercom_handset_audio" }, "device_placement": null, "geolocation": null, "last_location": null, "siren": null, "is_e2ee": false, "had_subscription": false, "owner_id": "762490876" } ] python-ring-doorbell-0.8.12/tests/fixtures/ring_intercom_settings.json000066400000000000000000000245521463726264600263750ustar00rootroot00000000000000{ "type": "intercom_handset_audio", "advanced_motion_settings": { "zone_1": { "name": "Default Zone", "state": 2, "vertex1": { "x": 0, "y": 0.4 }, "vertex2": { "x": 0.333333, "y": 0.4 }, "vertex3": { "x": 0.666666, "y": 0.4 }, "vertex4": { "x": 1, "y": 0.4 }, "vertex5": { "x": 1, "y": 1 }, "vertex6": { "x": 0.666666, "y": 1 }, "vertex7": { "x": 0.333333, "y": 1 }, "vertex8": { "x": 0, "y": 1 } }, "zone_2": { "name": "Zone 2", "state": 0, "vertex1": { "x": 0, "y": 0 }, "vertex2": { "x": 0, "y": 0 }, "vertex3": { "x": 0, "y": 0 }, "vertex4": { "x": 0, "y": 0 }, "vertex5": { "x": 0, "y": 0 }, "vertex6": { "x": 0, "y": 0 }, "vertex7": { "x": 0, "y": 0 }, "vertex8": { "x": 0, "y": 0 } }, "zone_3": { "name": "Zone 3", "state": 0, "vertex1": { "x": 0, "y": 0 }, "vertex2": { "x": 0, "y": 0 }, "vertex3": { "x": 0, "y": 0 }, "vertex4": { "x": 0, "y": 0 }, "vertex5": { "x": 0, "y": 0 }, "vertex6": { "x": 0, "y": 0 }, "vertex7": { "x": 0, "y": 0 }, "vertex8": { "x": 0, "y": 0 } } }, "backend_settings": { "live_view_preset_profile": "middle", "motion_snooze_preset_profile": "low", "enable_rich_notifications": false, "terms_of_service_accepted": { "autoreply": false, "concierge": false }, "paid_features": { "alexa_concierge": true, "cv_triggers": true, "human": true, "loitering": true, "motion": true, "other_motion": true, "package_delivery": true, "package_pickup": true, "sheila_cv": true, "sheila_recording": true }, "features_confirmed": 5 }, "chime_settings": { "enable": true, "type": 2, "duration": 10 }, "motion_settings": { "motion_detection_enabled": true, "advanced_motion_detection_enabled": true, "advanced_motion_detection_mode": "edge", "advanced_motion_detection_human_only_mode": false, "advanced_motion_detection_loitering_mode": false, "advanced_motion_zones_enabled": true, "advanced_motion_zones_type": "8vertices", "advanced_pir_motion_zones": { "zone1_sensitivity": 5, "zone2_sensitivity": 5, "zone3_sensitivity": 5, "zone4_sensitivity": 5, "zone5_sensitivity": 5, "zone6_sensitivity": 5 }, "loitering_threshold": 10, "enable_recording": true, "end_detection": 20, "advanced_motion_recording_human_mode": false, "advanced_motion_glance_enabled": false, "zone_settings_v2_enabled": true, "motion_snooze_profile": [ 1, 5, 15 ] }, "pir_settings": { "sensitivity_1": 5, "sensitivity_2": 5, "sensitivity_3": 5, "zone_enable": 31, "zone_mask": 0 }, "stream_settings": { "profile": 2, "active_streaming_profile": "rms", "streaming_profiles": { "freeswitch": {}, "rms": { "host": "rms-eu-west-1.rapi.us-east-1.prod.client.cap.ring.devices.a2z.com", "port": 443 } } }, "video_settings": { "exposure_control": 2, "night_color_enable": false, "hdr_enable": false, "clip_length_max": 60, "clip_length_min": 10, "ae_mode": 0, "ignore_zones": { "zone1": { "name": "undefined", "state": 0, "vertex1": { "x": 0, "y": 0 }, "vertex2": { "x": 0, "y": 0 } }, "zone2": { "name": "undefined", "state": 0, "vertex1": { "x": 0, "y": 0 }, "vertex2": { "x": 0, "y": 0 } }, "zone3": { "name": "undefined", "state": 0, "vertex1": { "x": 0, "y": 0 }, "vertex2": { "x": 0, "y": 0 } }, "zone4": { "name": "undefined", "state": 0, "vertex1": { "x": 0, "y": 0 }, "vertex2": { "x": 0, "y": 0 } } }, "encryption_enabled": false, "encryption_method": 1 }, "vod_settings": { "enable": true, "toggled_at": "2016-08-01T00:00:00+00:00", "use_cached_vod_domain": false }, "volume_settings": { "doorbell_volume": 6, "mic_volume": 11, "voice_volume": 11 }, "general_settings": { "enable_audio_recording": true, "lite_24x7_enabled": false, "offline_motion_event_enabled": false, "lite_24x7_subscribed": true, "offline_motion_event_subscribed": false, "firmwares_locked": false, "utc_offset": "+01:00", "theft_alarm_enable": false, "wrapup_domain": "wu.ring.com", "use_wrapup_domain": false, "data_collection_enabled": false, "log_selected_sink": 0, "country_code": "IT" }, "snapshot_settings": { "frequency_secs": 3600, "lite_24x7_resolution_p": 360, "ome_resolution_p": 360, "max_upload_kb": 5000, "frequency_after_secs": 2, "period_after_secs": 30, "close_container": 1 }, "client_device_settings": { "ringtones_enabled": false, "people_only_enabled": false, "advanced_motion_enabled": false, "motion_message_enabled": false, "shadow_correction_enabled": false, "night_vision_enabled": false, "light_schedule_enabled": false, "rich_notifications_eligible": false, "show_24x7_lite": false, "show_offline_motion_events": false, "cfes_eligible": false, "show_radar_data": false, "motion_zone_recommendation": false, "ptz_setup_complete": false, "local_playback_enabled": false, "dynamic_network_switching_eligible": false, "missing_key_delivery_address": false }, "light_snooze_settings": { "duration": 0 }, "cv_settings": { "detection_types": { "human": { "enabled": false, "mode": "none", "notification": false }, "loitering": { "enabled": false, "mode": "none", "notification": false }, "motion": { "enabled": true, "mode": "edge", "notification": true }, "moving_vehicle": { "enabled": false, "mode": "none", "notification": false }, "nearby_pom": { "enabled": false, "mode": "none", "notification": false }, "other_motion": { "enabled": false, "mode": "none", "notification": false }, "package_delivery": { "enabled": false, "mode": "none", "notification": false }, "package_pickup": { "enabled": false, "mode": "none", "notification": false } }, "threshold": { "loitering": 10, "package_delivery": 2 } }, "concierge_settings": { "mode": "disabled", "alexa_settings": { "delay_ms": 10000 }, "autoreply_settings": { "delay_ms": 10000 } }, "schedule_settings": {}, "intercom_settings": { "predecessor": "{\"make\":\"Comelit\",\"model\":\"2738W\",\"wires\":2}", "config": "{\"intercom_type\": 2, \"number_of_wires\": 2, \"autounlock_enabled\": false, \"speaker_gain\": [-49, -35, -25, -21, -16, -9, -6, -3, 0, 3, 6, 9], \"digital\": {\"audio_amp\": 0, \"chg_en\": false, \"fast_chg\": false, \"bypass\": false, \"idle_lvl\": 32, \"ext_audio\": false, \"ext_audio_term\": 0, \"off_hk_tm\": 0, \"unlk_ka\": false, \"unlock\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"ring\": {\"cap_tm\": 40, \"rpl_tm\": 200, \"gain\": 2000, \"cmp_thr\": 4500, \"lvl\": 28000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"m\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_off\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_on\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}}}", "intercom_type": "DF", "ring_to_open": false, "unlock_mode": 0, "replication": 1 }, "sheila_settings": { "cv_processing_enabled": false, "local_storage_enabled": false }, "keep_alive_settings": { "keep_alive_auto": 45 }, "lite_24x7": { "mode": "cloud", "mode_properties": { "sheila": {} } }, "zone_settings": { "motion": [ { "id": "718bd4c3-a4e4-4460-8118-90098d5f237a", "name": "Default Zone", "state": "enabled", "properties": { "detection_types": [ "motion" ] }, "vertices": [ { "x": 0, "y": 0.4 }, { "x": 0.333333, "y": 0.4 }, { "x": 0.666666, "y": 0.4 }, { "x": 1, "y": 0.4 }, { "x": 1, "y": 1 }, { "x": 0.666666, "y": 1 }, { "x": 0.333333, "y": 1 }, { "x": 0, "y": 1 } ] } ] }, "ptz_settings": {}, "thermometer_settings": {}, "auth_settings": { "fallback_enabled": false, "protocol": "basic", "retry_interval": 3600 }, "attestation_settings": { "rda_enabled": true } } python-ring-doorbell-0.8.12/tests/fixtures/ring_intercom_users.json000066400000000000000000000012251463726264600256660ustar00rootroot00000000000000[ { "id": 115201490, "verified": true, "first_name": "John", "last_name": "Carter", "email": "john@cart.er", "object_type": "user", "devices": [ { "id": 185036587, "role": "owner", "device_type": "intercom_handset_audio", "permissions": null } ] }, { "id": 194872097, "verified": true, "first_name": "Bob", "last_name": "Meloni", "email": "bob@melo.ni", "object_type": "user", "devices": [ { "id": 185036587, "role": "shared_user", "device_type": "intercom_handset_audio", "permissions": null } ] } ] python-ring-doorbell-0.8.12/tests/fixtures/ring_listen_credentials.json000066400000000000000000000005331463726264600265010ustar00rootroot00000000000000{ "gcm": { "token": "foobar", "appId": "foobar", "androidId": "5678901234567890123", "securityToken": "0123456789012345678" }, "keys": { "public": "foobar", "private": "foobar", "secret": "foobar" }, "fcm": { "token": "foobar", "pushSet": "foobar" } }python-ring-doorbell-0.8.12/tests/fixtures/ring_listen_ding.json000066400000000000000000000015021463726264600251220ustar00rootroot00000000000000{ "ding": { "streaming_protocol": "ring_media_server", "riid": "0123456789abcdef0123456789abcdef", "created_at": "2023-10-24T08:51:23.395Z", "e2ee_method": 1, "location_id": "abcXyz-123_987", "device_name": "Front Door", "doorbot_id": 12345678, "e2ee_enabled": false, "streaming_data_hash": "sh-v1|1228_characters_urlsafe_b64", "device_kind": "floodlight_v2", "id": 12345678901234, "request_id": "abcd1234-cd12-f321-123a-abcdef123456", "properties": { "active_streaming_profile": "rms", "is_sidewalk": false } }, "aps": { "alert": "🔔 Someone is at your Front Door", "sound": "DoorBot.wav" }, "subtype": "ding", "action": "com.ring.push.HANDLE_NEW_DING" } python-ring-doorbell-0.8.12/tests/fixtures/ring_listen_fcmdata.json000066400000000000000000000002651463726264600256050ustar00rootroot00000000000000{ "data": { "gcmData": "ring_listen_gcmdata.json" }, "from": "123456789012", "priority": "high", "fcmMessageId": "1234fdeb-1234-bc78-cd12-abcdef123456" }python-ring-doorbell-0.8.12/tests/fixtures/ring_listen_intercom_unlock.json000066400000000000000000000004561463726264600274030ustar00rootroot00000000000000{ "aps": { "alert": "Your Ingresso in Casetta was used to unlock the entrance on 2\\/2\\/24 at 12:10 PM" }, "action": "com.ring.push.INTERCOM_UNLOCK_FROM_APP", "alarm_meta": { "device_zid": 185036587, "location_id": "1234abcd-12cd-123f-de12-0123456789ab" } }python-ring-doorbell-0.8.12/tests/fixtures/ring_listen_motion.json000066400000000000000000000017121463726264600255110ustar00rootroot00000000000000{ "ding": { "streaming_protocol": "ring_media_server", "riid": "0123456789abcdef0123456789abcdef", "created_at": "2023-10-24T08:51:23.395Z", "e2ee_method": 1, "location_id": "1234abcd-12cd-123f-de12-0123456789ab", "device_name": "Front Floodcam", "doorbot_id": 12345678, "e2ee_enabled": false, "streaming_data_hash": "sh-v1|1228_characters_urlsafe_b64", "device_kind": "floodlight_v2", "detection_type": "null", "id": 12345678901234, "request_id": "abcd1234-cd12-f321-123a-abcdef123456", "image_uuid": "abcd1234-cd12-f321-123a-abcdef123456:12345678", "properties": { "active_streaming_profile": "rms", "is_sidewalk": false } }, "aps": { "alert": "There is motion at your Front Floodcam", "sound": "Motion.wav" }, "subtype": "human", "action": "com.ring.push.HANDLE_NEW_motion" }python-ring-doorbell-0.8.12/tests/fixtures/ring_oauth.json000066400000000000000000000003111463726264600237400ustar00rootroot00000000000000{ "access_token": "eyJ0eWfvEQwqfJNKyQ9999", "token_type": "bearer", "expires_in": 3600, "refresh_token": "67695a26bdefc1ac8999", "scope": "client", "created_at": 1529099870 } python-ring-doorbell-0.8.12/tests/fixtures/ring_session.json000066400000000000000000000023401463726264600243070ustar00rootroot00000000000000{ "profile": { "authentication_token": "12345678910", "email": "foo@bar.org", "features": { "chime_dnd_enabled": false, "chime_pro_enabled": true, "delete_all_enabled": true, "delete_all_settings_enabled": false, "device_health_alerts_enabled": true, "floodlight_cam_enabled": true, "live_view_settings_enabled": true, "lpd_enabled": true, "lpd_motion_announcement_enabled": false, "multiple_calls_enabled": true, "multiple_delete_enabled": true, "nw_enabled": true, "nw_larger_area_enabled": false, "nw_user_activated": false, "owner_proactive_snoozing_enabled": true, "power_cable_enabled": false, "proactive_snoozing_enabled": false, "reactive_snoozing_enabled": false, "remote_logging_format_storing": false, "remote_logging_level": 1, "ringplus_enabled": true, "starred_events_enabled": true, "stickupcam_setup_enabled": true, "subscriptions_enabled": true, "ujet_enabled": false, "video_search_enabled": false, "vod_enabled": false}, "first_name": "Home", "id": 999999, "last_name": "Assistant"} } python-ring-doorbell-0.8.12/tests/ruff.toml000066400000000000000000000012211463726264600206750ustar00rootroot00000000000000# This extends our general Ruff rules specifically for tests extend = "../pyproject.toml" lint.extend-select = [ "PT", # Use @pytest.fixture without parentheses ] lint.extend-ignore = [ "S101", # Use of assert detected. As these are tests... "S105", # Detection of passwords... "S106", # Detection of passwords... "SLF001", # Tests will access private/protected members... "TCH002", # pytest doesn't like this one... "PLR0913", # we're overwriting function that has many arguments "ANN001", # type annotations - TODO "ANN201", # return type annotations - TODO "D103", # docstring - TODO "ARG001", # Unused function argument - TODO ] python-ring-doorbell-0.8.12/tests/test_cli.py000066400000000000000000000175201463726264600212270ustar00rootroot00000000000000"""Module for cli tests.""" from __future__ import annotations import json from pathlib import Path from typing import Any from unittest.mock import DEFAULT import pytest from asyncclick.testing import CliRunner from ring_doorbell import AuthenticationError, Requires2FAError, Ring from ring_doorbell.cli import ( _event_handler, cli, devices_command, list_command, listen, motion_detection, show, videos, ) from ring_doorbell.const import GCM_TOKEN_FILE from ring_doorbell.listen import can_listen from tests.conftest import load_fixture async def test_cli_default(ring): runner = CliRunner() with runner.isolated_filesystem(): res = await runner.invoke( cli, ["--username", "foo", "--password", "foo"], obj=ring ) expected = ( "Name: Downstairs\nFamily: chimes\nID:" " 999999\nTimezone: America/New_York\nWifi Name:" " ring_mock_wifi\nWifi RSSI: -39\n\n" ) assert res.exit_code == 0 assert expected in res.output async def test_show(ring): runner = CliRunner() with runner.isolated_filesystem(): res = await runner.invoke(show, obj=ring) expected = ( "Name: Downstairs\nFamily: chimes\nID:" " 999999\nTimezone: America/New_York\nWifi Name:" " ring_mock_wifi\nWifi RSSI: -39\n\n" ) assert res.exit_code == 0 assert expected in res.output async def test_devices(ring): runner = CliRunner() with runner.isolated_filesystem(): res = await runner.invoke(devices_command, obj=ring) expected = ( "(Pretty format coming soon, if you want json " "consistently from this command provide the --json flag)\n" ) for device_type in ring.devices_data: for device_api_id in ring.devices_data[device_type]: expected += ( json.dumps(ring.devices_data[device_type][device_api_id], indent=2) + "\n" ) assert res.exit_code == 0 assert expected == res.output async def test_list(ring): runner = CliRunner() with runner.isolated_filesystem(): res = await runner.invoke(list_command, obj=ring) expected = ( "Front Door (lpd_v1)\nBack Door (lpd_v1)\nDownstairs (chime)\n" "Front (hp_cam_v1)\nIngress (intercom_handset_audio)\n" ) assert res.exit_code == 0 assert expected in res.output async def test_videos(ring, mocker): runner = CliRunner() with runner.isolated_filesystem(): m = mocker.mock_open() ptch = mocker.patch("pathlib.Path.open", m, create=True) res = await runner.invoke(videos, ["--count", "--download-all"], obj=ring) assert ptch.mock_calls[2].args[0] == b"123456" assert "Downloading 3 videos" in res.output @pytest.mark.parametrize( ("affect_method", "exception", "file_exists"), [ (None, None, False), ("ring_doorbell.auth.Auth.fetch_token", Requires2FAError, False), ("ring_doorbell.ring.Ring.update_data", AuthenticationError, True), ], ids=("No 2FA", "Require 2FA", "Invalid Grant"), ) async def test_auth(mocker, affect_method, exception, file_exists): call_count = 0 def _raise_once(self, *_: dict[str, Any], **__: dict[str, Any]) -> dict[str, Any]: nonlocal call_count, exception if call_count == 0: call_count += 1 msg = "Simulated exception" raise exception(msg) call_count += 1 if hasattr(self, "_update_data"): return self._update_data() return DEFAULT mocker.patch.object(Path, "is_file", return_value=file_exists) mocker.patch.object(Path, "read_text", return_value=load_fixture("ring_oauth.json")) mocker.patch("builtins.input", return_value="Foo") mocker.patch("getpass.getpass", return_value="Foo") if affect_method is not None: mocker.patch(affect_method, side_effect=_raise_once, autospec=True) runner = CliRunner() with runner.isolated_filesystem(): res = await runner.invoke(cli) assert res.exit_code == 0 async def test_motion_detection(ring, requests_mock): runner = CliRunner() with runner.isolated_filesystem(): res = await runner.invoke( motion_detection, ["--device-name", "Front"], obj=ring, ) expected = "Front (hp_cam_v1) has motion detection off" assert res.exit_code == 0 assert expected in res.output res = await runner.invoke( motion_detection, ["--device-name", "Front", "--off"], obj=ring, ) expected = "Front (hp_cam_v1) already has motion detection off" assert res.exit_code == 0 assert expected in res.output # Changes the return to indicate that the siren is now on. requests_mock.get( "https://api.ring.com/clients_api/ring_devices", text=load_fixture("ring_devices_updated.json"), ) res = await runner.invoke( motion_detection, ["--device-name", "Front", "--on"], obj=ring, ) expected = "Front (hp_cam_v1) motion detection set to on" assert res.exit_code == 0 assert expected in res.output @pytest.mark.skipif( can_listen is False, reason="requires the extra [listen] to be installed" ) @pytest.mark.nolistenmock() async def test_listen_store_credentials(mocker, auth): runner = CliRunner() import firebase_messaging credentials = json.loads(load_fixture("ring_listen_credentials.json")) with runner.isolated_filesystem(): mocker.patch( "firebase_messaging.fcmpushclient.gcm_check_in", return_value="foobar" ) mocker.patch( "firebase_messaging.FcmPushClient.register", return_value=credentials ) mocker.patch("firebase_messaging.FcmPushClient.start") mocker.patch("firebase_messaging.FcmPushClient.is_started", return_value=True) ring = Ring(auth) assert not Path(GCM_TOKEN_FILE).is_file() await runner.invoke(listen, ["--store-credentials"], obj=ring) assert Path(GCM_TOKEN_FILE).is_file() assert firebase_messaging.fcmpushclient.gcm_check_in.call_count == 0 assert firebase_messaging.FcmPushClient.register.call_count == 1 assert firebase_messaging.FcmPushClient.start.call_count == 1 ring = Ring(auth) await runner.invoke(listen, ["--store-credentials"], obj=ring) assert firebase_messaging.fcmpushclient.gcm_check_in.call_count == 1 assert firebase_messaging.FcmPushClient.register.call_count == 1 assert firebase_messaging.FcmPushClient.start.call_count == 2 @pytest.mark.skipif( can_listen is False, reason="requires the extra [listen] to be installed" ) async def test_listen_event_handler(mocker, auth): from ring_doorbell.listen import RingEventListener ring = Ring(auth) listener = RingEventListener(ring) listener.start() listener.add_notification_callback(_event_handler(ring).on_event) msg = json.loads(load_fixture("ring_listen_fcmdata.json")) gcmdata = load_fixture("ring_listen_motion.json") msg["data"]["gcmData"] = gcmdata echomock = mocker.patch("ring_doorbell.cli.echo") mocker.patch( "ring_doorbell.cli.get_now_str", return_value="2023-10-24 09:42:18.789709" ) listener._on_notification(msg, "1234567") exp = ( "2023-10-24 09:42:18.789709: RingEvent(id=12345678901234, " "doorbot_id=12345678, device_name='Front Floodcam'" ", device_kind='floodlight_v2', now=1698137483.395," " expires_in=180, kind='motion', state='human') : " "Currently active count = 1" ) echomock.assert_called_with(exp) python-ring-doorbell-0.8.12/tests/test_listen.py000066400000000000000000000167711463726264600217650ustar00rootroot00000000000000"""The tests for the Ring platform.""" import asyncio import datetime import json import pytest from ring_doorbell import Ring from ring_doorbell.exceptions import RingError from ring_doorbell.listen import can_listen from tests.conftest import load_fixture # test_module.py pytestmark = pytest.mark.skipif( can_listen is False, reason=("requires the extra [listen] to be installed") ) if can_listen: from ring_doorbell.listen import RingEventListener async def test_listen(auth, mocker): import firebase_messaging disconnectmock = mocker.patch("firebase_messaging.FcmPushClient.stop") ring = Ring(auth) listener = RingEventListener(ring) listener.start() assert firebase_messaging.FcmPushClient.checkin.call_count == 1 assert firebase_messaging.FcmPushClient.start.call_count == 1 assert listener.subscribed is True assert listener.started is True with pytest.raises(RingError, match="ID 10 is not a valid callback id"): listener.remove_notification_callback(10) with pytest.raises( RingError, match="Cannot remove the default callback for ring-doorbell with value 1", ): listener.remove_notification_callback(1) cbid = listener.add_notification_callback(lambda: 2) del listener._callbacks[1] listener.remove_notification_callback(cbid) disconnectmock.assert_called() async def test_active_dings(auth, mocker): import firebase_messaging ring = Ring(auth) listener = RingEventListener(ring) listener.start() assert firebase_messaging.FcmPushClient.checkin.call_count == 1 assert firebase_messaging.FcmPushClient.start.call_count == 1 assert listener.subscribed is True assert listener.started is True num_active = len(ring.active_alerts()) assert num_active == 3 alertstoadd = 2 for i in range(alertstoadd): msg = json.loads(load_fixture("ring_listen_fcmdata.json")) gcmdata_dict = json.loads(load_fixture("ring_listen_ding.json")) created_at = datetime.datetime.utcnow().isoformat(timespec="milliseconds") + "Z" # noqa: DTZ003 gcmdata_dict["ding"]["created_at"] = created_at gcmdata_dict["ding"]["id"] = gcmdata_dict["ding"]["id"] + i msg["data"]["gcmData"] = json.dumps(gcmdata_dict) listener._on_notification(msg, "1234567" + str(i)) dings = ring.active_alerts() assert len(dings) == num_active + alertstoadd # Test with the same id which should overwrite # previous and keep the overall count the same for i in range(alertstoadd): msg = json.loads(load_fixture("ring_listen_fcmdata.json")) gcmdata_dict = json.loads(load_fixture("ring_listen_ding.json")) created_at = datetime.datetime.utcnow().isoformat(timespec="milliseconds") + "Z" # noqa: DTZ003 gcmdata_dict["ding"]["created_at"] = created_at gcmdata_dict["ding"]["id"] = gcmdata_dict["ding"]["id"] + 1 msg["data"]["gcmData"] = json.dumps(gcmdata_dict) listener._on_notification(msg, "1234567" + str(i)) dings = ring.active_alerts() assert len(dings) == num_active + alertstoadd listener.stop() async def test_intercom_unlock(auth, mocker): import firebase_messaging ring = Ring(auth) listener = RingEventListener(ring) listener.start() assert firebase_messaging.FcmPushClient.checkin.call_count == 1 assert firebase_messaging.FcmPushClient.start.call_count == 1 assert listener.subscribed is True assert listener.started is True num_active = len(ring.active_alerts()) assert num_active == 3 alertstoadd = 2 for i in range(alertstoadd): msg = json.loads(load_fixture("ring_listen_fcmdata.json")) gcmdata_dict = json.loads(load_fixture("ring_listen_intercom_unlock.json")) msg["data"]["gcmData"] = json.dumps(gcmdata_dict) listener._on_notification(msg, "1234567" + str(i)) dings = ring.active_alerts() assert len(dings) == num_active + alertstoadd listener.stop() @pytest.mark.nolistenmock() async def test_listen_subscribe_fail(auth, mocker, requests_mock, caplog): checkinmock = mocker.patch( "firebase_messaging.FcmPushClient.checkin", return_value="foobar" ) connectmock = mocker.patch("firebase_messaging.FcmPushClient.start") mocker.patch("firebase_messaging.FcmPushClient.is_started", return_value=True) requests_mock.patch( "https://api.ring.com/clients_api/device", status_code=401, content=b"foobar", ) ring = Ring(auth) listener = RingEventListener(ring) listener.start() # Check in gets and error so register is called assert checkinmock.call_count == 1 assert listener.subscribed is False assert listener.started is False assert connectmock.call_count == 0 exp = ( "Unable to checkin to listen service, " "response was 401 foobar, event listener not started" ) assert ( len( [ record for record in caplog.records if record.levelname == "ERROR" and record.message == exp ] ) == 1 ) @pytest.mark.nolistenmock() async def test_listen_gcm_fail(auth, mocker, requests_mock, caplog): # Check in gets and error so register is called, the subscribe gets an error credentials = json.loads(load_fixture("ring_listen_credentials.json")) checkinmock = mocker.patch( "firebase_messaging.fcmpushclient.gcm_check_in", return_value=None ) registermock = mocker.patch( "firebase_messaging.FcmPushClient.register", return_value=credentials ) connectmock = mocker.patch("firebase_messaging.FcmPushClient.start") mocker.patch("firebase_messaging.FcmPushClient.is_started", return_value=True) ring = Ring(auth) listener = RingEventListener(ring, credentials) listener.start() # Check in gets and error so register is called assert checkinmock.call_count == 1 assert registermock.call_count == 1 assert listener.subscribed is True assert listener.started is True assert connectmock.call_count == 1 @pytest.mark.nolistenmock() async def test_listen_fcm_fail(auth, mocker, requests_mock, caplog): checkinmock = mocker.patch( "firebase_messaging.FcmPushClient.checkin", return_value=None ) connectmock = mocker.patch("firebase_messaging.FcmPushClient.start") mocker.patch("firebase_messaging.FcmPushClient.is_started", return_value=True) ring = Ring(auth) listener = RingEventListener(ring) listener.start() # Check in gets and error so register is called assert checkinmock.call_count == 1 assert listener.subscribed is False assert listener.started is False assert connectmock.call_count == 0 exp = "Unable to check in to fcm, event listener not started" assert ( len( [ record for record in caplog.records if record.levelname == "ERROR" and record.message == exp ] ) == 1 ) def test_no_event_loop(auth): ring = Ring(auth) listener = RingEventListener(ring) listener.start() ring.update_dings() assert listener.started is True async def test_run_in_executor(auth): import firebase_messaging ring = Ring(auth) listener = RingEventListener(ring) loop = asyncio.get_running_loop() await loop.run_in_executor(None, listener.start) assert listener.started assert firebase_messaging.FcmPushClient.checkin.call_count == 1 assert firebase_messaging.FcmPushClient.start.call_count == 1 python-ring-doorbell-0.8.12/tests/test_other.py000066400000000000000000000072711463726264600216030ustar00rootroot00000000000000"""The tests for the Ring platform.""" def test_other_attributes(ring): """Test the Ring Other class and methods.""" dev = ring.devices()["other"][0] assert dev.id != 99999 assert dev.device_id == "124ba1b3fe1a" assert dev.kind == "intercom_handset_audio" assert dev.model == "Intercom" assert dev.location_id == "mock-location-id" assert dev.has_capability("battery") is False assert dev.has_capability("open") is True assert dev.has_capability("history") is True assert dev.timezone == "Europe/Rome" assert dev.battery_life == 52 assert dev.doorbell_volume == 8 assert dev.mic_volume == 11 assert dev.clip_length_max == 60 assert dev.connection_status == "online" assert len(dev.allowed_users) == 2 assert dev.subscribed is True assert dev.has_subscription is True assert dev.unlock_duration is None assert dev.keep_alive_auto == 45.0 assert isinstance(dev.history(limit=1, kind="on_demand"), list) assert len(dev.history(kind="ding")) == 1 assert len(dev.history(limit=1, kind="on_demand")) == 2 assert ( len(dev.history(limit=1, kind="on_demand", enforce_limit=True, retry=50)) == 1 ) dev.update_health_data() assert dev.wifi_name == "ring_mock_wifi" assert dev.wifi_signal_category == "good" assert dev.wifi_signal_strength != 100 def test_other_controls(ring, requests_mock): dev = ring.devices()["other"][0] dev.doorbell_volume = 6 history = list(filter(lambda x: x.method == "PUT", requests_mock.request_history)) assert history[0].path == "/clients_api/doorbots/185036587" assert history[0].query == "doorbot%5bsettings%5d%5bdoorbell_volume%5d=6" dev.mic_volume = 10 dev.voice_volume = 9 dev.clip_length_max = 30 dev.keep_alive_auto = 32.2 history = list(filter(lambda x: x.method == "PATCH", requests_mock.request_history)) assert history[0].path == "/devices/v1/devices/185036587/settings" assert history[0].text == '{"volume_settings": {"mic_volume": 10}}' assert history[1].path == "/devices/v1/devices/185036587/settings" assert history[1].text == '{"volume_settings": {"voice_volume": 9}}' assert history[2].path == "/devices/v1/devices/185036587/settings" assert history[2].text == '{"video_settings": {"clip_length_max": 30}}' assert history[3].path == "/devices/v1/devices/185036587/settings" assert history[3].text == '{"keep_alive_settings": {"keep_alive_auto": 32.2}}' def test_other_invitations(ring, requests_mock): dev = ring.devices()["other"][0] dev.invite_access("test@example.com") history = list(filter(lambda x: x.method == "POST", requests_mock.request_history)) assert history[2].path == "/clients_api/locations/mock-location-id/invitations" assert history[2].text == ( '{"invitation": {"doorbot_ids": [185036587],' ' "invited_email": "test@example.com", "group_ids": []}}' ) dev.remove_access(123456789) history = list( filter(lambda x: x.method == "DELETE", requests_mock.request_history) ) assert ( history[0].path == "/clients_api/locations/mock-location-id/invitations/123456789" ) def test_other_open_door(ring, requests_mock, mocker): dev = ring.devices()["other"][0] mocker.patch("uuid.uuid4", return_value="987654321") dev.open_door(15) history = list(filter(lambda x: x.method == "PUT", requests_mock.request_history)) assert history[0].path == "/commands/v1/devices/185036587/device_rpc" assert history[0].text == ( '{"command_name": "device_rpc", "request": ' '{"id": "987654321", "jsonrpc": "2.0", "method": "unlock_door", "params": ' '{"door_id": 0, "user_id": 15}}}' ) python-ring-doorbell-0.8.12/tests/test_ring.py000066400000000000000000000124271463726264600214200ustar00rootroot00000000000000"""The tests for the Ring platform.""" import pytest from ring_doorbell import RingError def test_basic_attributes(ring): """Test the Ring class and methods.""" data = ring.devices() assert len(data["chimes"]) == 1 assert len(data["doorbots"]) == 1 assert len(data["authorized_doorbots"]) == 1 assert len(data["stickup_cams"]) == 1 assert len(data["other"]) == 1 def test_chime_attributes(ring): """Test the Ring Chime class and methods.""" dev = ring.devices()["chimes"][0] assert dev.address == "123 Main St" assert dev.id != 99999 assert dev.device_id == "abcdef123" assert dev.kind == "chime" assert dev.model == "Chime" assert dev.has_capability("battery") is False assert dev.has_capability("volume") is True assert dev.has_capability("history") is False assert dev.latitude is not None assert dev.timezone == "America/New_York" assert dev.volume == 2 assert len(dev.history()) == 0 dev.update_health_data() assert dev.wifi_name == "ring_mock_wifi" assert dev.wifi_signal_category == "good" assert dev.wifi_signal_strength != 100 def test_doorbell_attributes(ring): data = ring.devices() dev = data["doorbots"][0] assert dev.name == "Front Door" assert dev.id == 987652 assert dev.address == "123 Main St" assert dev.kind == "lpd_v1" assert dev.model == "Doorbell Pro" assert dev.has_capability("battery") is False assert dev.has_capability("volume") is True assert dev.has_capability("history") is True assert dev.longitude == -70.12345 assert dev.timezone == "America/New_York" assert dev.volume == 1 assert dev.has_subscription is True assert dev.connection_status == "online" assert isinstance(dev.history(limit=1, kind="motion"), list) assert len(dev.history(kind="ding")) == 1 assert len(dev.history(limit=1, kind="motion")) == 2 assert len(dev.history(limit=1, kind="motion", enforce_limit=True, retry=50)) == 1 assert dev.existing_doorbell_type == "Mechanical" dev.update_health_data() assert dev.wifi_name == "ring_mock_wifi" assert dev.wifi_signal_category == "good" assert dev.wifi_signal_strength == -58 def test_shared_doorbell_attributes(ring): data = ring.devices() dev = data["authorized_doorbots"][0] assert dev.id == 987653 assert dev.battery_life == 51 assert dev.address == "123 Second St" assert dev.kind == "lpd_v1" assert dev.model == "Doorbell Pro" assert dev.has_capability("battery") is False assert dev.has_capability("volume") is True assert dev.has_capability("history") is True assert dev.longitude == -70.12345 assert dev.timezone == "America/New_York" assert dev.volume == 5 assert dev.existing_doorbell_type == "Digital" def test_stickup_cam_attributes(ring): dev = ring.devices()["stickup_cams"][0] assert dev.kind == "hp_cam_v1" assert dev.model == "Floodlight Cam" assert dev.has_capability("battery") is False assert dev.has_capability("light") is True assert dev.has_capability("history") is True assert dev.lights == "off" assert dev.siren == 0 def test_stickup_cam_controls(ring, requests_mock): dev = ring.devices()["stickup_cams"][0] dev.lights = "off" dev.lights = "on" dev.siren = 0 dev.siren = 30 history = list(filter(lambda x: x.method == "PUT", requests_mock.request_history)) assert history[0].path == "/clients_api/doorbots/987652/floodlight_light_off" assert history[1].path == "/clients_api/doorbots/987652/floodlight_light_on" assert history[2].path == "/clients_api/doorbots/987652/siren_off" assert "duration" not in history[2].qs assert history[3].path == "/clients_api/doorbots/987652/siren_on" assert history[3].qs["duration"][0] == "30" def test_light_groups(ring): group = ring.groups()["mock-group-id"] assert group.name == "Landscape" assert group.family == "group" assert group.group_id == "mock-group-id" assert group.location_id == "mock-location-id" assert group.model == "Light Group" assert group.has_capability("light") is True with pytest.raises(RingError): group.has_capability("something-else") assert group.lights is False # Attempt turning on lights group.lights = True # Attempt turning off lights group.lights = False # Attempt turning on lights for 30 seconds group.lights = (True, 30) # Attempt setting lights to invalid value with pytest.raises(RingError): group.lights = 30 def test_motion_detection_enable(ring, requests_mock): dev = ring.devices()["doorbots"][0] dev.motion_detection = True dev.motion_detection = False history = list(filter(lambda x: x.method == "PATCH", requests_mock.request_history)) assert history[len(history) - 2].path == "/devices/v1/devices/987652/settings" assert ( history[len(history) - 2].text == '{"motion_settings": {"motion_detection_enabled": true}}' ) assert history[len(history) - 1].path == "/devices/v1/devices/987652/settings" assert ( history[len(history) - 1].text == '{"motion_settings": {"motion_detection_enabled": false}}' ) active_dings = ring.active_alerts() assert len(active_dings) == 3 assert len(ring.active_alerts()) == 3 python-ring-doorbell-0.8.12/tox.ini000066400000000000000000000014111463726264600172100ustar00rootroot00000000000000[tox] envlist = py38, py39, py310, py311, py312, lint, docs skip_missing_interpreters = True isolated_build = true [testenv] skip_install = true allowlist_externals = poetry commands_pre = poetry install --sync --extras listen commands = poetry run pytest tests/ --cov=ring_doorbell --cov-report=xml --cov-report=term-missing --import-mode importlib [testenv:lint] skip_install = true allowlist_externals = poetry commands_pre = poetry install --sync --extras listen --verbose ignore_errors = True commands = poetry run pre-commit run --all-files [testenv:docs] skip_install = true allowlist_externals = poetry make commands_pre = poetry install --sync --extras docs --without dev ignore_errors = True commands = poetry run make -C docs html