pax_global_header00006660000000000000000000000064145603630430014516gustar00rootroot0000000000000052 comment=0778de4356ca46dc2fa24b0e6318e4e198d99560 python-ring-doorbell-0.8.7/000077500000000000000000000000001456036304300156105ustar00rootroot00000000000000python-ring-doorbell-0.8.7/.flake8000066400000000000000000000006711456036304300167670ustar00rootroot00000000000000[flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build doctests = True # To work with Black max-line-length = 88 # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring # W504 line break after binary operator ignore = W503, E203, D202, W504 per-file-ignores = # imported but unused & redefinition of unused tests/*: F811, F401python-ring-doorbell-0.8.7/.github/000077500000000000000000000000001456036304300171505ustar00rootroot00000000000000python-ring-doorbell-0.8.7/.github/release-drafter.yml000066400000000000000000000000541456036304300227370ustar00rootroot00000000000000template: | ## What's Changed $CHANGES python-ring-doorbell-0.8.7/.github/workflows/000077500000000000000000000000001456036304300212055ustar00rootroot00000000000000python-ring-doorbell-0.8.7/.github/workflows/ci.yml000066400000000000000000000062141456036304300223260ustar00rootroot00000000000000name: CI on: push: branches: ["master"] pull_request: branches: ["master"] env: POETRY_VERSION: 1.7.1 TOX_VERSION: 4.11.3 COVERALLS_VERSION: 3.3.1 jobs: linting: name: "Perform linting checks" runs-on: ubuntu-latest strategy: matrix: python-version: ["3.12"] steps: - uses: "actions/checkout@v2" - uses: "actions/setup-python@v2" with: python-version: "${{ matrix.python-version }}" - name: "Install dependencies" run: | python3 -m pip install --upgrade pip python3 -m pip install --user pipx python3 -m pipx ensurepath pipx install poetry==$POETRY_VERSION pipx install tox==$TOX_VERSION - name: Lint with tox run: | tox env: TOXENV: lint docs: name: "Build docs" needs: linting runs-on: ubuntu-latest strategy: matrix: python-version: ["3.12"] steps: - uses: "actions/checkout@v2" - uses: "actions/setup-python@v2" with: python-version: "${{ matrix.python-version }}" - name: "Install dependencies" run: | python3 -m pip install --upgrade pip python3 -m pip install --user pipx python3 -m pipx ensurepath pipx install poetry==$POETRY_VERSION pipx install tox==$TOX_VERSION - name: Make docs with tox run: | tox env: TOXENV: docs tests: name: tests needs: linting runs-on: ubuntu-latest strategy: fail-fast: true matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python3 -m pip install --upgrade pip python3 -m pip install --user pipx python3 -m pipx ensurepath pipx install poetry==$POETRY_VERSION pipx install tox==$TOX_VERSION pipx install coveralls==$COVERALLS_VERSION - name: Prepare toxenv id: toxenv run: | if [[ '${{ matrix.python-version }}' == '3.8' ]]; then echo "::set-output name=toxenv::py38" elif [[ '${{ matrix.python-version }}' == '3.9' ]]; then echo "::set-output name=toxenv::py39" elif [[ '${{ matrix.python-version }}' == '3.10' ]]; then echo "::set-output name=toxenv::py310" elif [[ '${{ matrix.python-version }}' == '3.11' ]]; then echo "::set-output name=toxenv::py311" else echo "::set-output name=toxenv::py312" fi - name: Test with tox run: | tox env: TOXENV: ${{ steps.toxenv.outputs.toxenv }} - 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.7/.github/workflows/pythonpublish.yml000066400000000000000000000015421456036304300246420ustar00rootroot00000000000000# 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.7/.github/workflows/release-drafter.yml000066400000000000000000000005101456036304300247710ustar00rootroot00000000000000name: 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.7/.gitignore000066400000000000000000000014261456036304300176030ustar00rootroot00000000000000# ---> Python # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Visual Studio .vs # Distribution / packaging .Python env/ 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.7/.hound.yml000066400000000000000000000000311456036304300175200ustar00rootroot00000000000000python: enabled: false python-ring-doorbell-0.8.7/.pre-commit-config.yaml000066400000000000000000000004141456036304300220700ustar00rootroot00000000000000repos: - repo: https://github.com/python-poetry/poetry rev: 1.6.0 hooks: - id: poetry-check - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format python-ring-doorbell-0.8.7/.readthedocs.yaml000066400000000000000000000021741456036304300210430ustar00rootroot00000000000000# 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.7/CHANGELOG.rst000066400000000000000000000002401456036304300176250ustar00rootroot00000000000000========= Changelog ========= An up to date dynamic changelog can found on `readthedocs `_. python-ring-doorbell-0.8.7/CONTRIBUTING.rst000066400000000000000000000111631456036304300202530ustar00rootroot00000000000000============ 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.7/LICENSE000066400000000000000000000167441456036304300166310ustar00rootroot00000000000000 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.7/README.rst000066400000000000000000000205671456036304300173110ustar00rootroot00000000000000===================== 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 json import getpass from pathlib import Path from pprint import pprint from ring_doorbell import Ring, Auth from oauthlib.oauth2 import MissingTokenError cache_file = Path("test_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 main(): if cache_file.is_file(): auth = Auth("MyProject/1.0", json.loads(cache_file.read_text()), token_updated) else: username = input("Username: ") password = getpass.getpass("Password: ") auth = Auth("MyProject/1.0", None, token_updated) try: auth.fetch_token(username, password) except MissingTokenError: auth.fetch_token(username, password, otp_callback()) ring = Ring(auth) ring.update_data() devices = ring.devices() pprint(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.7/RELEASING.rst000066400000000000000000000015121456036304300176520ustar00rootroot00000000000000========= 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.7/_config.yml000066400000000000000000000000331456036304300177330ustar00rootroot00000000000000theme: jekyll-theme-minimalpython-ring-doorbell-0.8.7/docs/000077500000000000000000000000001456036304300165405ustar00rootroot00000000000000python-ring-doorbell-0.8.7/docs/Makefile000066400000000000000000000011761456036304300202050ustar00rootroot00000000000000# 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.7/docs/make.bat000066400000000000000000000014441456036304300201500ustar00rootroot00000000000000@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.7/docs/source/000077500000000000000000000000001456036304300200405ustar00rootroot00000000000000python-ring-doorbell-0.8.7/docs/source/changelog.rst000066400000000000000000000004001456036304300225130ustar00rootroot00000000000000========= 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.7/docs/source/conf.py000066400000000000000000000024411456036304300213400ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html 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" 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 = [] # -- 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.7/docs/source/contributing.rst000066400000000000000000000000431456036304300232760ustar00rootroot00000000000000.. include:: ../../CONTRIBUTING.rstpython-ring-doorbell-0.8.7/docs/source/index.rst000066400000000000000000000007311456036304300217020ustar00rootroot00000000000000.. 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.7/poetry.lock000066400000000000000000003052711456036304300200140ustar00rootroot00000000000000# 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.0.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" [package.extras] doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.22)"] [[package]] name = "astroid" version = "3.0.1" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ {file = "astroid-3.0.1-py3-none-any.whl", hash = "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca"}, {file = "astroid-3.0.1.tar.gz", hash = "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e"}, ] [package.dependencies] typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "asyncclick" version = "8.1.3.4" description = "Composable command line interface toolkit, async version" optional = false python-versions = ">=3.7" files = [ {file = "asyncclick-8.1.3.4-py3-none-any.whl", hash = "sha256:f8db604e37dabd43922d58f857817b1dfd8f88695b75c4cc1afe7ff1cc238a7b"}, {file = "asyncclick-8.1.3.4.tar.gz", hash = "sha256:81d98cbf6c8813f9cd5599f586d56cfc532e9e6441391974d10827abb90fe833"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "babel" version = "2.13.0" description = "Internationalization utilities" optional = true python-versions = ">=3.7" files = [ {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"}, {file = "Babel-2.13.0.tar.gz", hash = "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210"}, ] [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 = "black" version = "23.10.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ {file = "black-23.10.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:f8dc7d50d94063cdfd13c82368afd8588bac4ce360e4224ac399e769d6704e98"}, {file = "black-23.10.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:f20ff03f3fdd2fd4460b4f631663813e57dc277e37fb216463f3b907aa5a9bdd"}, {file = "black-23.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3d9129ce05b0829730323bdcb00f928a448a124af5acf90aa94d9aba6969604"}, {file = "black-23.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:960c21555be135c4b37b7018d63d6248bdae8514e5c55b71e994ad37407f45b8"}, {file = "black-23.10.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:30b78ac9b54cf87bcb9910ee3d499d2bc893afd52495066c49d9ee6b21eee06e"}, {file = "black-23.10.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:0e232f24a337fed7a82c1185ae46c56c4a6167fb0fe37411b43e876892c76699"}, {file = "black-23.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31946ec6f9c54ed7ba431c38bc81d758970dd734b96b8e8c2b17a367d7908171"}, {file = "black-23.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:c870bee76ad5f7a5ea7bd01dc646028d05568d33b0b09b7ecfc8ec0da3f3f39c"}, {file = "black-23.10.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:6901631b937acbee93c75537e74f69463adaf34379a04eef32425b88aca88a23"}, {file = "black-23.10.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:481167c60cd3e6b1cb8ef2aac0f76165843a374346aeeaa9d86765fe0dd0318b"}, {file = "black-23.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74892b4b836e5162aa0452393112a574dac85e13902c57dfbaaf388e4eda37c"}, {file = "black-23.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:47c4510f70ec2e8f9135ba490811c071419c115e46f143e4dce2ac45afdcf4c9"}, {file = "black-23.10.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:76baba9281e5e5b230c9b7f83a96daf67a95e919c2dfc240d9e6295eab7b9204"}, {file = "black-23.10.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:a3c2ddb35f71976a4cfeca558848c2f2f89abc86b06e8dd89b5a65c1e6c0f22a"}, {file = "black-23.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db451a3363b1e765c172c3fd86213a4ce63fb8524c938ebd82919bf2a6e28c6a"}, {file = "black-23.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:7fb5fc36bb65160df21498d5a3dd330af8b6401be3f25af60c6ebfe23753f747"}, {file = "black-23.10.0-py3-none-any.whl", hash = "sha256:e223b731a0e025f8ef427dd79d8cd69c167da807f5710add30cdf131f13dd62e"}, {file = "black-23.10.0.tar.gz", hash = "sha256:31b9f87b277a68d0e99d2905edae08807c007973eaa609da5f0c62def6b7c0bd"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cachetools" version = "5.3.1" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, ] [[package]] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] name = "cffi" version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, ] [package.dependencies] pycparser = "*" [[package]] name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "chardet" version = "5.2.0" description = "Universal encoding detector for Python 3" optional = false python-versions = ">=3.7" files = [ {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, ] [[package]] name = "charset-normalizer" version = "3.3.0" 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.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"}, {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"}, {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, ] [[package]] name = "click" version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "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.3.2" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "cryptography" version = "41.0.6" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:0f27acb55a4e77b9be8d550d762b0513ef3fc658cd3eb15110ebbcbd626db12c"}, {file = "cryptography-41.0.6-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ae236bb8760c1e55b7a39b6d4d32d2279bc6c7c8500b7d5a13b6fb9fc97be35b"}, {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afda76d84b053923c27ede5edc1ed7d53e3c9f475ebaf63c68e69f1403c405a8"}, {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da46e2b5df770070412c46f87bac0849b8d685c5f2679771de277a422c7d0b86"}, {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff369dd19e8fe0528b02e8df9f2aeb2479f89b1270d90f96a63500afe9af5cae"}, {file = "cryptography-41.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b648fe2a45e426aaee684ddca2632f62ec4613ef362f4d681a9a6283d10e079d"}, {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5daeb18e7886a358064a68dbcaf441c036cbdb7da52ae744e7b9207b04d3908c"}, {file = "cryptography-41.0.6-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:068bc551698c234742c40049e46840843f3d98ad7ce265fd2bd4ec0d11306596"}, {file = "cryptography-41.0.6-cp37-abi3-win32.whl", hash = "sha256:2132d5865eea673fe6712c2ed5fb4fa49dba10768bb4cc798345748380ee3660"}, {file = "cryptography-41.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:48783b7e2bef51224020efb61b42704207dde583d7e371ef8fc2a5fb6c0aabc7"}, {file = "cryptography-41.0.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8efb2af8d4ba9dbc9c9dd8f04d19a7abb5b49eab1f3694e7b5a16a5fc2856f5c"}, {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5a550dc7a3b50b116323e3d376241829fd326ac47bc195e04eb33a8170902a9"}, {file = "cryptography-41.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:85abd057699b98fce40b41737afb234fef05c67e116f6f3650782c10862c43da"}, {file = "cryptography-41.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f39812f70fc5c71a15aa3c97b2bbe213c3f2a460b79bd21c40d033bb34a9bf36"}, {file = "cryptography-41.0.6-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:742ae5e9a2310e9dade7932f9576606836ed174da3c7d26bc3d3ab4bd49b9f65"}, {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:35f3f288e83c3f6f10752467c48919a7a94b7d88cc00b0668372a0d2ad4f8ead"}, {file = "cryptography-41.0.6-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d03186af98b1c01a4eda396b137f29e4e3fb0173e30f885e27acec8823c1b09"}, {file = "cryptography-41.0.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b27a7fd4229abef715e064269d98a7e2909ebf92eb6912a9603c7e14c181928c"}, {file = "cryptography-41.0.6-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:398ae1fc711b5eb78e977daa3cbf47cec20f2c08c5da129b7a296055fbb22aed"}, {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7e00fb556bda398b99b0da289ce7053639d33b572847181d6483ad89835115f6"}, {file = "cryptography-41.0.6-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:60e746b11b937911dc70d164060d28d273e31853bb359e2b2033c9e93e6f3c43"}, {file = "cryptography-41.0.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3288acccef021e3c3c10d58933f44e8602cf04dba96d9796d70d537bb2f4bbc4"}, {file = "cryptography-41.0.6.tar.gz", hash = "sha256:422e3e31d63743855e43e5a6fcc8b4acab860f560f9321b0ee6269cc7ed70cc3"}, ] [package.dependencies] cffi = ">=1.12" [package.extras] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] nox = ["nox"] pep8test = ["black", "check-sdist", "mypy", "ruff"] sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] [[package]] name = "dill" version = "0.3.7" description = "serialize all of Python" optional = false python-versions = ">=3.7" files = [ {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, ] [package.extras] graph = ["objgraph (>=1.7.2)"] [[package]] name = "distlib" version = "0.3.7" description = "Distribution utilities" optional = false python-versions = "*" files = [ {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, ] [[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.1.3" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" version = "3.12.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, ] [package.extras] docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] typing = ["typing-extensions (>=4.7.1)"] [[package]] name = "firebase-messaging" version = "0.2.0" description = "FCM/GCM push notification client" optional = true python-versions = ">=3.8,<4.0" files = [ {file = "firebase_messaging-0.2.0-py3-none-any.whl", hash = "sha256:7be0bb21debfa7324a62c1c1000e034fb26e89b499e8938b84c04f33779284ad"}, {file = "firebase_messaging-0.2.0.tar.gz", hash = "sha256:18ad828a7ee2ce7f1972b129b38442d7b698f1f8d32347e5531c64d722322ff1"}, ] [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 = "flake8" version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.6.1" files = [ {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.9.0,<2.10.0" pyflakes = ">=2.5.0,<2.6.0" [[package]] name = "http-ece" version = "1.1.0" description = "Encrypted Content Encoding for HTTP" optional = true python-versions = "*" files = [ {file = "http_ece-1.1.0.tar.gz", hash = "sha256:932ebc2fa7c216954c320a188ae9c1f04d01e67bec9cdce1bfbc912813b0b4f8"}, ] [package.dependencies] cryptography = ">=2.5" [[package]] name = "identify" version = "2.5.30" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"}, {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] [[package]] name = "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 = "6.8.0" description = "Read metadata from Python packages" optional = true python-versions = ">=3.8" files = [ {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "isort" version = "5.12.0" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" files = [ {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, ] [package.extras] colors = ["colorama (>=0.4.3)"] pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] [[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.3" description = "Safely add untrusted strings to HTML/XML markup." optional = true python-versions = ">=3.7" files = [ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] [[package]] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] [[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 = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "nodeenv" version = "1.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 = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.7" files = [ {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] [[package]] name = "platformdirs" version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" files = [ {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, ] [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "pluggy" version = "1.3.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" version = "3.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.24.3" description = "" optional = true python-versions = ">=3.7" files = [ {file = "protobuf-4.24.3-cp310-abi3-win32.whl", hash = "sha256:20651f11b6adc70c0f29efbe8f4a94a74caf61b6200472a9aea6e19898f9fcf4"}, {file = "protobuf-4.24.3-cp310-abi3-win_amd64.whl", hash = "sha256:3d42e9e4796a811478c783ef63dc85b5a104b44aaaca85d4864d5b886e4b05e3"}, {file = "protobuf-4.24.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:6e514e8af0045be2b56e56ae1bb14f43ce7ffa0f68b1c793670ccbe2c4fc7d2b"}, {file = "protobuf-4.24.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:ba53c2f04798a326774f0e53b9c759eaef4f6a568ea7072ec6629851c8435959"}, {file = "protobuf-4.24.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:f6ccbcf027761a2978c1406070c3788f6de4a4b2cc20800cc03d52df716ad675"}, {file = "protobuf-4.24.3-cp37-cp37m-win32.whl", hash = "sha256:1b182c7181a2891e8f7f3a1b5242e4ec54d1f42582485a896e4de81aa17540c2"}, {file = "protobuf-4.24.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b0271a701e6782880d65a308ba42bc43874dabd1a0a0f41f72d2dac3b57f8e76"}, {file = "protobuf-4.24.3-cp38-cp38-win32.whl", hash = "sha256:e29d79c913f17a60cf17c626f1041e5288e9885c8579832580209de8b75f2a52"}, {file = "protobuf-4.24.3-cp38-cp38-win_amd64.whl", hash = "sha256:067f750169bc644da2e1ef18c785e85071b7c296f14ac53e0900e605da588719"}, {file = "protobuf-4.24.3-cp39-cp39-win32.whl", hash = "sha256:2da777d34b4f4f7613cdf85c70eb9a90b1fbef9d36ae4a0ccfe014b0b07906f1"}, {file = "protobuf-4.24.3-cp39-cp39-win_amd64.whl", hash = "sha256:f631bb982c5478e0c1c70eab383af74a84be66945ebf5dd6b06fc90079668d0b"}, {file = "protobuf-4.24.3-py3-none-any.whl", hash = "sha256:f6f8dc65625dadaad0c8545319c2e2f0424fede988368893ca3844261342c11a"}, {file = "protobuf-4.24.3.tar.gz", hash = "sha256:12e9ad2ec079b833176d2921be2cb24281fa591f0b119b208b788adc48c2561d"}, ] [[package]] name = "pycodestyle" version = "2.9.1" description = "Python style guide checker" optional = false python-versions = ">=3.6" files = [ {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, ] [[package]] name = "pycparser" version = "2.21" description = "C parser in Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] [[package]] name = "pyflakes" version = "2.5.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.6" files = [ {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] [[package]] name = "pygments" version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." optional = true python-versions = ">=3.7" files = [ {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, ] [package.extras] plugins = ["importlib-metadata"] [[package]] name = "pylint" version = "3.0.1" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ {file = "pylint-3.0.1-py3-none-any.whl", hash = "sha256:9c90b89e2af7809a1697f6f5f93f1d0e518ac566e2ac4d2af881a69c13ad01ea"}, {file = "pylint-3.0.1.tar.gz", hash = "sha256:81c6125637be216b4652ae50cc42b9f8208dfb725cdc7e04c48f6902f4dbdf40"}, ] [package.dependencies] astroid = ">=3.0.0,<=3.1.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, ] isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] [[package]] name = "pyproject-api" version = "1.6.1" description = "API to interact with the python pyproject.toml based projects" optional = false python-versions = ">=3.8" files = [ {file = "pyproject_api-1.6.1-py3-none-any.whl", hash = "sha256:4c0116d60476b0786c88692cf4e325a9814965e2469c5998b830bba16b183675"}, {file = "pyproject_api-1.6.1.tar.gz", hash = "sha256:1817dc018adc0d1ff9ca1ed8c60e1623d5aaca40814b953af14a9cf9a5cae538"}, ] [package.dependencies] packaging = ">=23.1" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [package.extras] docs = ["furo (>=2023.8.19)", "sphinx (<7.2)", "sphinx-autodoc-typehints (>=1.24)"] testing = ["covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "setuptools (>=68.1.2)", "wheel (>=0.41.2)"] [[package]] name = "pytest" version = "7.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" version = "0.21.1" description = "Pytest support for asyncio" optional = false python-versions = ">=3.7" files = [ {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, ] [package.dependencies] pytest = ">=7.0.0" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" files = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, ] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytest-mock" version = "3.11.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.7" files = [ {file = "pytest-mock-3.11.1.tar.gz", hash = "sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"}, {file = "pytest_mock-3.11.1-py3-none-any.whl", hash = "sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39"}, ] [package.dependencies] pytest = ">=5.0" [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 = "2023.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] [[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.11.0" description = "Mock out responses from the requests package" optional = false python-versions = "*" files = [ {file = "requests-mock-1.11.0.tar.gz", hash = "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4"}, {file = "requests_mock-1.11.0-py2.py3-none-any.whl", hash = "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15"}, ] [package.dependencies] requests = ">=2.3,<3" six = "*" [package.extras] fixture = ["fixtures"] test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testtools"] [[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 = "setuptools" version = "68.2.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, ] [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-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] [[package]] name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] [[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.2.1" description = "Build a sphinx changelog from GitHub Releases" optional = true python-versions = ">=3.7,<4.0" files = [ {file = "sphinx_github_changelog-1.2.1-py3-none-any.whl", hash = "sha256:27b8906220c8010f116b61c90980f84d12f9446fb144f7c575c124e9c92e6c46"}, {file = "sphinx_github_changelog-1.2.1.tar.gz", hash = "sha256:20affb353bf02c89b868c2e5b9c61693dd22f3d2b2e134063c49f9e0d69352a5"}, ] [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 = "tomlkit" version = "0.12.1" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, ] [[package]] name = "tox" version = "4.11.3" description = "tox is a generic virtualenv management and test command line tool" optional = false python-versions = ">=3.8" files = [ {file = "tox-4.11.3-py3-none-any.whl", hash = "sha256:599af5e5bb0cad0148ac1558a0b66f8fff219ef88363483b8d92a81e4246f28f"}, {file = "tox-4.11.3.tar.gz", hash = "sha256:5039f68276461fae6a9452a3b2c7295798f00a0e92edcd9a3b78ba1a73577951"}, ] [package.dependencies] cachetools = ">=5.3.1" chardet = ">=5.2" colorama = ">=0.4.6" filelock = ">=3.12.3" packaging = ">=23.1" platformdirs = ">=3.10" pluggy = ">=1.3" pyproject-api = ">=1.6.1" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} virtualenv = ">=20.24.3" [package.extras] docs = ["furo (>=2023.8.19)", "sphinx (>=7.2.4)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.24)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=1)", "diff-cover (>=7.7)", "distlib (>=0.3.7)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.18)", "psutil (>=5.9.5)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.12)", "wheel (>=0.41.2)"] [[package]] name = "typing-extensions" version = "4.8.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] [[package]] name = "urllib3" version = "2.0.7" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.7" files = [ {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" version = "20.24.5" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<4" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "zipp" version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = true python-versions = ">=3.8" files = [ {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [extras] docs = ["sphinx", "sphinx-github-changelog", "sphinx-rtd-theme"] listen = ["firebase-messaging"] [metadata] lock-version = "2.0" python-versions = "^3.8" content-hash = "5a0289d7837829f26da51ae589718313616bc10558e8b58921f3f8dae1f25e32" python-ring-doorbell-0.8.7/pyproject.toml000066400000000000000000000060341456036304300205270ustar00rootroot00000000000000[tool.poetry] name = "ring-doorbell" version = "0.8.7" description = "A Python library to communicate with Ring Door Bell (https://ring.com/)" authors = ["Marcelo Moreira de Mello "] license = "LGPLv3+" 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 = [ "LICENSE", "CONTRIBUTING.rst", "CHANGELOG.rst", "scripts/ringcli.py" ] [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.0", optional = true} [tool.poetry.group.dev.dependencies] flake8 = "*" mock = "*" pre-commit = "*" pylint = "*" pytest = "*" pytest-cov = "*" requests-mock = "*" tox = "*" pytest-asyncio = "*" pytest-mock = "*" black = "*" pytest-socket = "^0.6.0" [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 = "--disable-socket --allow-unix-socket" 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" select = [ "E", # pycodestyle # "D", # pydocstyle "F", # pyflakes "UP", # pyupgrade "B", # flake8-bugbear "SIM", # flake8-simplify "I", # isort "S", # bandit ] ignore = [ "D105", # Missing docstring in magic method "D107", # Missing docstring in `__init__` ] [tool.ruff.pydocstyle] convention = "pep257" [tool.ruff.per-file-ignores] "tests/*.py" = [ "D100", "D101", "D102", "D103", "D104", "F401", "S101", # allow asserts "E501", # ignore line-too-longs ] "docs/source/conf.py" = [ "D100", "D103", ] [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" python-ring-doorbell-0.8.7/ring_doorbell/000077500000000000000000000000001456036304300204315ustar00rootroot00000000000000python-ring-doorbell-0.8.7/ring_doorbell/__init__.py000066400000000000000000000016341456036304300225460ustar00rootroot00000000000000"""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.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 from ring_doorbell.stickup_cam import RingStickUpCam __all__ = [ "Ring", "Auth", "RingChime", "RingStickUpCam", "RingLightGroup", "RingDoorBell", "RingOther", "RingEvent", "RingError", "AuthenticationError", "Requires2FAError", "RingTimeout", "RingGeneric", "RingEvent", ] python-ring-doorbell-0.8.7/ring_doorbell/auth.py000066400000000000000000000123301456036304300217430ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Auth Class.""" import uuid from json import dumps as json_dumps from oauthlib.oauth2 import ( LegacyApplicationClient, MissingTokenError, OAuth2Error, TokenExpiredError, ) from requests import HTTPError, 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, token=None, token_updater=None, hardware_id=None): """ :type token: Optional[Dict[str, str]] :type token_updater: Optional[Callable[[str], None]] """ self.user_agent = user_agent self.hardware_id = hardware_id if self.hardware_id is None: # 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, password, otp_code=None): """Initial token fetch 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): """Refreshes 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): """Get hardware ID.""" return self.hardware_id def get_device_model(self): """Get device model.""" return self.device_model def query( self, url, method="GET", extra_params=None, data=None, json=None, timeout=None, raise_for_status=True, ): """Query data from Ring API.""" if timeout is None: timeout = TIMEOUT params = {} if extra_params: params.update(extra_params) kwargs = { "params": params, "headers": {"User-Agent": self.user_agent}, "timeout": timeout, } if method in ["POST", "PUT"]: if json is not None: kwargs["json"] = json kwargs["headers"]["Content-Type"] = "application/json" if data is not None: kwargs["data"] = data if method == "PATCH": # PATCH method of requests library does not have a json argument if json is not None: kwargs["data"] = json_dumps(json) kwargs["headers"]["Content-Type"] = "application/json" if data is not None: kwargs["data"] = data try: try: resp = getattr(self._oauth, method.lower())(url, **kwargs) except TokenExpiredError: self._oauth.token = self.refresh_tokens() resp = getattr(self._oauth, method.lower())(url, **kwargs) except AuthenticationError as ex: raise ex # refresh_tokens will return this error if not valid except Timeout as ex: raise RingTimeout(f"Timeout error during query of url {url}: {ex}") from ex except Exception as ex: raise RingError(f"Unknown error during query of url {url}: {ex}") 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: raise RingError( f"HTTP error with status code {resp.status_code} " + f"during query of url {url}: {ex}" ) from ex return resp python-ring-doorbell-0.8.7/ring_doorbell/chime.py000066400000000000000000000046661456036304300221040ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Chime wrapper.""" import logging 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, KIND_DING, LINKED_CHIMES_ENDPOINT, MSG_VOL_OUTBOUND, TESTSOUND_CHIME_ENDPOINT, ) from ring_doorbell.generic import RingGeneric _LOGGER = logging.getLogger(__name__) class RingChime(RingGeneric): """Implementation for Ring Chime.""" @property def family(self): """Return Ring device family type.""" return "chimes" def update_health_data(self): """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): """Return Ring device model name.""" if self.kind in CHIME_KINDS: return "Chime" if self.kind in CHIME_PRO_KINDS: return "Chime Pro" return None def has_capability(self, capability): """Return if device has specific capability.""" if capability == "volume": return True return False @property def volume(self): """Return if chime volume.""" return self._attrs.get("settings").get("volume") @volume.setter def volume(self, value): if not ((isinstance(value, int)) and (CHIME_VOL_MIN <= value <= CHIME_VOL_MAX)): _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(CHIME_VOL_MIN, CHIME_VOL_MAX)) return False 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() return True @property def linked_tree(self): """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=KIND_DING): """Play chime to test sound.""" if kind 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}) return True python-ring-doorbell-0.8.7/ring_doorbell/cli.py000066400000000000000000000457411456036304300215650ustar00rootroot00000000000000# vim:sw=4:ts=4:et # Many thanks to @troopermax """Python Ring command line interface.""" 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 import asyncclick as click from ring_doorbell import Auth, AuthenticationError, Requires2FAError, Ring, RingEvent from ring_doorbell.const import CLI_TOKEN_FILE, PACKAGE_NAME, USER_AGENT from ring_doorbell.listen import can_listen def _header(): _bar() echo("Ring CLI") def _bar(): echo("---------------------------------") click.anyio_backend = "asyncio" pass_ring = click.make_pass_decorator(Ring) cache_file = Path(CLI_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): 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: raise click.UsageError( "Illegal usage: `{}` is mutually exclusive with " "arguments `{}`.".format(self.name, ", ".join(self.mutually_exclusive)) ) return await super().handle_parse_result(ctx, opts, args) echo = click.echo def token_updated(token): """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"] ) filename = filename.replace(" ", "_").replace(":", ".") + ".mp4" return filename 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 if user_agent != USER_AGENT: cache_file = Path(user_agent + ".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) @cli.command(name="list") @pass_ring async def list_command(ring: Ring): """List ring devices.""" devices = ring.devices() doorbells = devices["doorbots"] chimes = devices["chimes"] stickup_cams = devices["stickup_cams"] other = devices["other"] for device in doorbells: echo(device) for device in chimes: echo(device) for device in stickup_cams: echo(device) for device in 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"{str(device)} is not capable of motion detection") return state = "on" if device.motion_detection else "off" if not turn_on and not turn_off: echo(f"{str(device)} has motion detection {state}") return is_on = device.motion_detection if (turn_on and is_on) or (turn_off and not is_on): echo(f"{str(device)} already has motion detection {state}") return device.motion_detection = turn_on if turn_on else False state = "on" if device.motion_detection else "off" echo(f"{str(device)} motion detection set to {state}") return @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 = 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() @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)) 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) ) @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): """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): """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 group in ring.groups_data: echo(json.dumps(group, 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): """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"], 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)) @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_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 # 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 count and not download and not download_all: echo("Last recording url is: " + device.recording_url(device.last_recording_id)) return 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" ) counter = 0 for event in events: counter += 1 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) async def ainput(string: str) -> str: loop = asyncio.get_event_loop() await loop.run_in_executor(None, lambda s=string: sys.stdout.write(s + " ")) def read_with_timeout(timeout): if select.select( [ sys.stdin, ], [], [], timeout, )[0]: # line = sys.stdin.next() line = sys.stdin.readline() return line 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): self.ring = ring def on_event(self, event: RingEvent): 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="credentials.json", help=( "File to store push credentials, " + "if not provided credentials will be recreated from scratch" ), ) @click.option( "--store-credentials/--no-store-credentials", default=False, 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 store the push credentials, default is false", ) @pass_ring @click.pass_context async def listen( ctx, ring, store_credentials, credentials_file, show_credentials, ): """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): 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) credentials = None if store_credentials and Path(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.7/ring_doorbell/const.py000066400000000000000000000142201456036304300221300ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Constants.""" class OAuth: """OAuth class constants""" ENDPOINT = "https://oauth.ring.com/oauth/token" CLIENT_ID = "ring_official_android" SCOPE = ["client"] 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 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" 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" # chime test sound kinds KIND_DING = "ding" KIND_MOTION = "motion" 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" 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.7/ring_doorbell/doorbot.py000066400000000000000000000417621456036304300224650ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Doorbell wrapper.""" import logging import os import time from datetime import datetime import pytz 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_DOORBELL_HISTORY, URL_RECORDING, URL_RECORDING_SHARE_PLAY, ) from ring_doorbell.exceptions import RingError from ring_doorbell.generic import RingGeneric _LOGGER = logging.getLogger(__name__) class RingDoorBell(RingGeneric): """Implementation for Ring Doorbell.""" def __init__(self, ring, device_api_id, shared=False): super().__init__(ring, device_api_id) self.shared = shared @property def family(self): """Return Ring device family type.""" return "authorized_doorbots" if self.shared else "doorbots" def update_health_data(self): """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): """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 None def has_capability(self, capability): """Return if device has specific capability.""" if capability == "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 == "knock": return self.kind in PEEPHOLE_CAM_KINDS if capability == "pre-roll": return self.kind in DOORBELL_3_PLUS_KINDS if capability == "volume": return True if capability == "history": return True if capability in ("motion_detection", "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): """Return battery life.""" if ( self._attrs.get("battery_life") is None and self._attrs.get("battery_life_2") is None ): return None value = 0 if "battery_life_2" in self._attrs: # Camera has two battery bays if self._attrs.get("battery_life") is not None: # Bay 1 value += int(self._attrs.get("battery_life")) if self._attrs.get("battery_life_2") is not None: # Bay 2 value += int(self._attrs.get("battery_life_2")) return value # Camera has a single battery bay # Latest stickup cam can be externally powered value = int(self._attrs.get("battery_life")) if value and value > 100: value = 100 return value @property def existing_doorbell_type(self): """ Return existing doorbell type. 0: Mechanical 1: Digital 2: Not Present """ try: return DOORBELL_EXISTING_TYPE[ self._attrs.get("settings").get("chime_settings").get("type") ] except AttributeError: return None @existing_doorbell_type.setter def existing_doorbell_type(self, value): """ Return existing doorbell type. 0: Mechanical 1: Digital 2: Not Present """ if value not in DOORBELL_EXISTING_TYPE: _LOGGER.error("%s", MSG_EXISTING_TYPE) return False 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() return True return None @property def existing_doorbell_type_enabled(self): """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._attrs.get("settings").get("chime_settings").get("enable") return False @existing_doorbell_type_enabled.setter def existing_doorbell_type_enabled(self, value): """Enable/disable the existing doorbell if Digital/Mechanical.""" if self.existing_doorbell_type: if not isinstance(value, bool): _LOGGER.error("%s", MSG_BOOLEAN_REQUIRED) return None if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[2]: return None 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() return True return False @property def existing_doorbell_type_duration(self): """Return duration for Digital chime.""" if ( self.existing_doorbell_type and self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[1] ): return self._attrs.get("settings").get("chime_settings").get("duration") return None @existing_doorbell_type_duration.setter def existing_doorbell_type_duration(self, value): """Set duration for Digital chime.""" if self.existing_doorbell_type: if not ( (isinstance(value, int)) and (DOORBELL_VOL_MIN <= value <= DOORBELL_VOL_MAX) ): _LOGGER.error( "%s", MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN, DOORBELL_VOL_MAX) ) return False 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() return True return None def history( self, limit=30, timezone=None, kind=None, enforce_limit=False, older_than=None, retry=8, *, convert_timezone=True, ): """ 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 """ 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": str(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 return response @property def last_recording_id(self): """Return the last recording ID.""" try: return self.history(limit=1)[0]["id"] except (IndexError, TypeError): return None @property def live_streaming_json(self): """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, filename=None, override=False, timeout=DEFAULT_VIDEO_DOWNLOAD_TIMEOUT, ): """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 False 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 and req.status_code == 200: if filename: if os.path.isfile(filename) and not override: _LOGGER.error("%s", FILE_EXISTS.format(filename)) return False with open(filename, "wb") as recording: recording.write(req.content) return True else: return req.content except OSError as error: msg = f"Error downloading recording {recording_id}: {error}" _LOGGER.error(msg) raise RingError(msg) from error return False def recording_url(self, recording_id): """Return HTTPS recording URL.""" if not self.has_subscription: msg = "Your Ring account does not have an active subscription." _LOGGER.warning(msg) return False 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 False @property def subscribed(self): """Return if is online.""" result = self._attrs.get("subscribed") if result is None: return False return True @property def subscribed_motion(self): """Return if is subscribed_motion.""" result = self._attrs.get("subscribed_motions") if result is None: return False return True @property def has_subscription(self): """Return boolean if the account has subscription.""" return self._attrs.get("features").get("show_recordings") @property def volume(self): """Return volume.""" return self._attrs.get("settings").get("doorbell_volume") @volume.setter def volume(self, value): if not ( (isinstance(value, int)) and (DOORBELL_VOL_MIN <= value <= DOORBELL_VOL_MAX) ): _LOGGER.error( "%s", MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN, DOORBELL_VOL_MAX) ) return False 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() return True @property def connection_status(self): """Return connection status.""" return self._attrs.get("alerts").get("connection") def get_snapshot(self, retries=3, delay=1, filename=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")), raw=True ).content if filename: with open(filename, "wb") as jpg: jpg.write(snapshot) return True return snapshot return False def _motion_detection_state(self): if "settings" in self._attrs and "motion_detection_enabled" in self._attrs.get( "settings" ): return self._attrs.get("settings")["motion_detection_enabled"] return None @property def motion_detection(self): """Return motion detection enabled state.""" return self._motion_detection_state() @motion_detection.setter def motion_detection(self, state): """Set the motion detection enabled state.""" values = [True, False] if state not in values: _LOGGER.error("%s", MSG_ALLOWED_VALUES.format(", ".join(values))) return False if self._motion_detection_state() is None: _LOGGER.warning( "%s", MSG_EXPECTED_ATTRIBUTE_NOT_FOUND.format( "settings[motion_detection_enabled]" ), ) return False 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() return True python-ring-doorbell-0.8.7/ring_doorbell/event.py000066400000000000000000000006521456036304300221270ustar00rootroot00000000000000"""Module for ring events.""" from dataclasses import dataclass @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): return getattr(self, key) def get(self, key): return getattr(self, key) if hasattr(self, key) else None python-ring-doorbell-0.8.7/ring_doorbell/exceptions.py000066400000000000000000000005511456036304300231650ustar00rootroot00000000000000"""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): """Exception for ring authentication errors.""" python-ring-doorbell-0.8.7/ring_doorbell/generic.py000066400000000000000000000063071456036304300224250ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring RingGeneric wrapper.""" # pylint: disable=useless-object-inheritance class RingGeneric: """Generic Implementation for Ring Chime/Doorbell.""" # pylint: disable=redefined-builtin # pylint:disable=invalid-name def __init__(self, ring, device_api_id): """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 = {} # alerts notifications self.alert_expires_at = None def __repr__(self): """Return __repr__.""" return f"<{self.__class__.__name__}: {self.name}>" def __str__(self): return f"{self.name} ({self.kind})" def update(self): """Update this device info.""" self.update_health_data() def update_health_data(self): """Update the health data.""" raise NotImplementedError @property def _attrs(self): """Return attributes.""" return self._ring.devices_data[self.family][self.device_api_id] @property def id(self): """Return ID.""" return self.device_api_id @property def name(self): """Return name.""" return self._attrs["description"] @property def device_id(self): """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 family(self): """Return Ring device family type.""" raise NotImplementedError @property def model(self): """Return Ring device model name.""" raise NotImplementedError def has_capability(self, capability): """Return if device has specific capability.""" return self.capability @property def address(self): """Return address.""" return self._attrs.get("address") @property def firmware(self): """Return firmware.""" return self._attrs.get("firmware_version") @property def latitude(self): """Return latitude attr.""" return self._attrs.get("latitude") @property def longitude(self): """Return longitude attr.""" return self._attrs.get("longitude") @property def kind(self): """Return kind attr.""" return self._attrs.get("kind") @property def timezone(self): """Return timezone.""" return self._attrs.get("time_zone") @property def wifi_name(self): """Return wifi ESSID name. Requires health data to be updated. """ return self._health_attrs.get("wifi_name") @property def wifi_signal_strength(self): """Return wifi RSSI. Requires health data to be updated. """ return self._health_attrs.get("latest_signal_strength") @property def wifi_signal_category(self): """Return wifi signal category. Requires health data to be updated. """ return self._health_attrs.get("latest_signal_category") python-ring-doorbell-0.8.7/ring_doorbell/group.py000066400000000000000000000056461456036304300221520ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring light group wrapper.""" import logging import warnings from ring_doorbell.const import GROUP_DEVICES_ENDPOINT, MSG_ALLOWED_VALUES _LOGGER = logging.getLogger(__name__) class RingLightGroup: """Implementation for RingLightGroup.""" # pylint:disable=invalid-name # pylint: disable=redefined-builtin def __init__(self, ring, group_id): """Initialize Ring Light Group.""" self._ring = ring self.group_id = group_id # pylint:disable=invalid-name self._health_attrs = {} self._health_attrs_fetched = False def __repr__(self): """Return __repr__.""" return f"<{self.__class__.__name__}: {self.name}>" def update(self): """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): """Return attributes.""" return self._ring.groups_data[self.group_id] @property def id(self): """Return ID.""" return self.group_id @property def name(self): """Return name.""" return self._attrs["name"] @property def family(self): """Return Ring device family type.""" return "group" @property def device_id(self): """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): """Return group location ID.""" return self._attrs["location_id"] @property def model(self): """Return Ring device model name.""" return "Light Group" def has_capability(self, capability): """Return if device has specific capability.""" if capability == "light": return True return False @property def lights(self): """Return lights status.""" if not self._health_attrs_fetched: self.update() return self._health_attrs["lights_on"] @lights.setter def lights(self, value): """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): _LOGGER.error("%s", MSG_ALLOWED_VALUES.format(", ".join(values))) return False url = GROUP_DEVICES_ENDPOINT.format(self.location_id, self.group_id) payload = {"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() return True python-ring-doorbell-0.8.7/ring_doorbell/listen/000077500000000000000000000000001456036304300217275ustar00rootroot00000000000000python-ring-doorbell-0.8.7/ring_doorbell/listen/__init__.py000066400000000000000000000006131456036304300240400ustar00rootroot00000000000000"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.7/ring_doorbell/listen/eventlistener.py000066400000000000000000000137051456036304300251760ustar00rootroot00000000000000"""Module for listening to firebase cloud messages and updating dings""" import json import logging import time from datetime import datetime from firebase_messaging import FcmPushClient from ring_doorbell.const import ( API_URI, API_VERSION, DEFAULT_LISTEN_EVENT_EXPIRES_IN, KIND_DING, KIND_MOTION, PUSH_ACTION_DING, PUSH_ACTION_MOTION, RING_SENDER_ID, SUBSCRIPTION_ENDPOINT, ) from ring_doorbell.exceptions import RingError from ring_doorbell.ring import Ring from ..event import RingEvent from .listenerconfig import RingEventListenerConfig _logger = logging.getLogger(__name__) class RingEventListener: """Class to connect to firebase cloud messaging.""" def __init__( self, ring: Ring, credentials=None, credentials_updated_callback=None, *, config: RingEventListenerConfig = RingEventListenerConfig.default_config, ): self._ring = ring self._callbacks = {} 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 = None self._config = config or RingEventListenerConfig.default_config self._subscription_counter = 1 def add_subscription_to_ring(self, token) -> bool: # "hardware_id": self.auth.get_hardware_id(), 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 def add_notification_callback(self, callback): sub_id = self._subscription_counter self._callbacks[sub_id] = callback self._subscription_counter += 1 return sub_id def remove_notification_callback(self, subscription_id): if subscription_id == 1: raise RingError( "Cannot remove the default callback for ring-doorbell with value 1" ) if subscription_id not in self._callbacks: raise RingError(f"ID {subscription_id} is not a valid callback id") del self._callbacks[subscription_id] if len(self._callbacks) == 0 and self._receiver: self._receiver.stop() self._receiver = None def stop(self): if self._receiver: self.started = False self._receiver.stop() self._receiver = None self._callbacks = {} def start(self, callback=None, *, listen_loop=None, callback_loop=None, timeout=30): if not callback: callback = self._ring.add_event_to_dings_data 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 on_notification(self, notification, persistent_id, obj=None): gcm_data_json = json.loads(notification["data"]["gcmData"]) if "ding" not in gcm_data_json: if "community_alert" not in gcm_data_json: _logger.warning( "Unexpected alert type in gcmData. Full message is:\n%s", json.dumps(notification), ) return ding = gcm_data_json["ding"] action = gcm_data_json["action"] subtype = gcm_data_json["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() re = 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, ) for callback in self._callbacks.values(): callback(re) python-ring-doorbell-0.8.7/ring_doorbell/listen/listenerconfig.py000066400000000000000000000010531456036304300253130ustar00rootroot00000000000000"""Module for RingEventListenerConfig.""" from firebase_messaging import FcmPushClientConfig class RingEventListenerConfig(FcmPushClientConfig): """Configuration class for event listener.""" @classmethod @property def default_config(cls) -> "RingEventListenerConfig": "Return the default configuration for listening to ring alerts." config = RingEventListenerConfig() config.server_heartbeat_interval = 60 config.client_heartbeat_interval = 120 config.monitor_interval = 15 return config python-ring-doorbell-0.8.7/ring_doorbell/other.py000066400000000000000000000210421456036304300221230ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Other (Intercom) wrapper.""" import json import logging import uuid 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, ) from ring_doorbell.generic import RingGeneric _LOGGER = logging.getLogger(__name__) class RingOther(RingGeneric): """Implementation for Ring Intercom.""" def __init__(self, ring, device_api_id, shared=False): super().__init__(ring, device_api_id) self.shared = shared @property def family(self): """Return Ring device family type.""" return "other" def update_health_data(self): """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): """Return Ring device model name.""" if self.kind in INTERCOM_KINDS: return "Intercom" return None def has_capability(self, capability): """Return if device has specific capability.""" if capability == "open": return self.kind in INTERCOM_KINDS return False @property def battery_life(self): """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): """Return if is online.""" if self.kind in INTERCOM_KINDS: result = self._attrs.get("subscribed") if result is None: return False return True return None @property def has_subscription(self): """Return boolean if the account has subscription.""" if self.kind in INTERCOM_KINDS: return self._attrs.get("features").get("show_recordings") return None @property def unlock_duration(self): """Return time unlock switch is held closed""" json.loads( self._attrs.get("settings").get("intercom_settings").get("config") ).get("analog", {}).get("unlock_duration") @property def doorbell_volume(self): """Return doorbell volume.""" if self.kind in INTERCOM_KINDS: return self._attrs.get("settings").get("doorbell_volume") return None @doorbell_volume.setter def doorbell_volume(self, value): if not ( (isinstance(value, int)) and (OTHER_DOORBELL_VOL_MIN <= value <= OTHER_DOORBELL_VOL_MAX) ): _LOGGER.error( "%s", MSG_VOL_OUTBOUND.format(OTHER_DOORBELL_VOL_MIN, OTHER_DOORBELL_VOL_MAX), ) return False 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() return True @property def keep_alive_auto(self): if self.kind in INTERCOM_KINDS: return self._attrs.get("settings").get("keep_alive_auto") return None @keep_alive_auto.setter def keep_alive_auto(self, value): 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() return True @property def mic_volume(self): """Return mic volume.""" if self.kind in INTERCOM_KINDS: return self._attrs.get("settings").get("mic_volume") return None @mic_volume.setter def mic_volume(self, value): if not ((isinstance(value, int)) and (MIC_VOL_MIN <= value <= MIC_VOL_MAX)): _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(MIC_VOL_MIN, MIC_VOL_MAX)) return False 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() return True @property def voice_volume(self): """Return voice volume.""" if self.kind in INTERCOM_KINDS: return self._attrs.get("settings").get("voice_volume") return None @voice_volume.setter def voice_volume(self, value): if not ((isinstance(value, int)) and (VOICE_VOL_MIN <= value <= VOICE_VOL_MAX)): _LOGGER.error("%s", MSG_VOL_OUTBOUND.format(VOICE_VOL_MIN, VOICE_VOL_MAX)) return False 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() return True @property def clip_length_max(self): # 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): 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() return True @property def connection_status(self): """Return connection status.""" if self.kind in INTERCOM_KINDS: return self._attrs.get("alerts").get("connection") return None @property def location_id(self): """Return location id.""" if self.kind in INTERCOM_KINDS: return self._attrs.get("location_id", None) return None @property def allowed_users(self): """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=-1): """Open the door""" if self.kind in INTERCOM_KINDS: url = INTERCOM_OPEN_ENDPOINT.format(self.device_api_id) request_id = str(uuid.uuid4()) # request_timestamp = int(time.time() * 1000) payload = { "command_name": "device_rpc", "request": { "id": request_id, "jsonrpc": "2.0", "method": "unlock_door", "params": { # "command_timeout": 5, "door_id": 0, # "issue_time": request_timestamp, "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): """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): """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.7/ring_doorbell/ring.py000066400000000000000000000164541456036304300217540ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Doorbell module.""" import logging from time import time from typing import List from ring_doorbell import RingEvent from ring_doorbell.auth import Auth from ring_doorbell.chime import RingChime from ring_doorbell.doorbot import RingDoorBell 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, NEW_SESSION_ENDPOINT, ) _logger = logging.getLogger(__name__) TYPES = { "stickup_cams": RingStickUpCam, "chimes": RingChime, "doorbots": RingDoorBell, "authorized_doorbots": lambda ring, description: RingDoorBell( ring, description, shared=True ), "other": RingOther, } # pylint: disable=useless-object-inheritance class Ring: """A Python Abstraction object to Ring Door Bell.""" def __init__(self, auth: Auth): """Initialize the Ring object.""" self.auth: Auth = auth self.session = None self.subscription = None self.devices_data = None self.chime_health_data = None self.doorbell_health_data = None self.dings_data = None self.push_dings_data = [] self.groups_data = None self.init_loop = None def update_data(self): """Update all data.""" self._update_data() def _update_data(self): 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): # 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): """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): """Update device data.""" if self.session is None: self.create_session() data = 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): """Update dings data.""" if self.session is None: self.create_session() self.dings_data = self._query(DINGS_ENDPOINT).json() def update_groups(self): """Update groups data.""" if self.session is None: self.create_session() # Get all locations locations = set() for devices in self.devices_data.values(): for dev in devices.values(): if "location_id" in dev: locations.add(dev["location_id"]) # Query for groups self.groups_data = {} locations.discard(None) for location in locations: data = self._query(GROUPS_ENDPOINT.format(location)).json() if data["device_groups"] is not None: for group in data["device_groups"]: self.groups_data[group["device_group_id"]] = group def query( self, url, method="GET", extra_params=None, data=None, json=None, timeout=None ): """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( self, url, method="GET", extra_params=None, data=None, json=None, timeout=None ): _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): """Get all devices.""" devices = {} for dev_type, convertor in TYPES.items(): devices[dev_type] = [ convertor(self, obj["id"]) for obj in self.devices_data.get(dev_type, {}).values() ] return devices def get_device_list(self): """Get a combined list of all devices.""" devices = self.devices() return ( devices["doorbots"] + devices["authorized_doorbots"] + devices["stickup_cams"] + devices["chimes"] + devices["other"] ) def get_device_by_name(self, device_name): """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)} device = ( None if device_name not in names_to_idx else all_devices[names_to_idx[device_name]] ) return device def video_devices(self): """Get all devices.""" devices = self.devices() return ( devices["doorbots"] + devices["authorized_doorbots"] + devices["stickup_cams"] ) def groups(self): """Get all groups.""" groups = {} for group_id in self.groups_data: groups[group_id] = RingLightGroup(self, group_id) return groups def active_alerts(self) -> List[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 = {} 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 alerts.values() python-ring-doorbell-0.8.7/ring_doorbell/stickup_cam.py000066400000000000000000000140061456036304300233060ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Doorbell wrapper.""" import logging from ring_doorbell.const import ( FLOODLIGHT_CAM_KINDS, FLOODLIGHT_CAM_PLUS_KINDS, FLOODLIGHT_CAM_PRO_KINDS, HEALTH_DOORBELL_ENDPOINT, 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, ) from ring_doorbell.doorbot import RingDoorBell _LOGGER = logging.getLogger(__name__) class RingStickUpCam(RingDoorBell): """Implementation for RingStickUpCam.""" @property def family(self): """Return Ring device family type.""" return "stickup_cams" def update_health_data(self): """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): """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 None def has_capability(self, capability): """Return if device has specific capability.""" if capability == "history": return True if capability == "battery": return self.kind in ( SPOTLIGHT_CAM_BATTERY_KINDS + STICKUP_CAM_KINDS + STICKUP_CAM_BATTERY_KINDS + STICKUP_CAM_GEN3_KINDS ) if capability == "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 == "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 ("motion_detection", "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): """Return lights status.""" return self._attrs.get("led_status") @lights.setter def lights(self, state): """Control the lights.""" values = ["on", "off"] if state not in values: _LOGGER.error("%s", MSG_ALLOWED_VALUES.format(", ".join(values))) return False url = LIGHTS_ENDPOINT.format(self.device_api_id, state) self._ring.query(url, method="PUT") self._ring.update_devices() return True @property def siren(self): """Return siren status.""" if self._attrs.get("siren_status"): return self._attrs.get("siren_status").get("seconds_remaining") return None @siren.setter def siren(self, duration): """Control the siren.""" if not ( (isinstance(duration, int)) and (SIREN_DURATION_MIN <= duration <= SIREN_DURATION_MAX) ): _LOGGER.error( "%s", MSG_VOL_OUTBOUND.format(SIREN_DURATION_MIN, SIREN_DURATION_MAX) ) return False 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() return True python-ring-doorbell-0.8.7/scripts/000077500000000000000000000000001456036304300172775ustar00rootroot00000000000000python-ring-doorbell-0.8.7/scripts/changeloggenerator.py000066400000000000000000000044401456036304300235110ustar00rootroot00000000000000"""Module to autogenerate the CHANGELOG.rst from GitHub Release Notes.""" import argparse import docutils.nodes as dn import docutils.utils as du from sphinx_github_changelog import changelog option_spec = { "changelog-url": "https://python-ring-doorbell.readthedocs.io/en/stable/#changelog", "github": "https://github.com/tchellomello/python-ring-doorbell/releases/", "pypi": "https://pypi.org/project/python-ring-doorbell/", } OUTPUT_FILENAME = "CHANGELOG.rst" class NodeVisitor(dn.SparseNodeVisitor): """Very basic class to output rst from docutils xml""" output = "" def output_reference(refuri, reftext): return "`" + reftext + " <" + refuri + ">`__ " def visit_section(self, node): pass def visit_title(self, node): title = node.astext() line = "=" * len(title) self.output += "\n\n" + node.astext() + "\n" + line + "\n\n" def visit_paragraph(self, node): for childnode in node.children[0].children: if type(childnode) == dn.reference: self.output += NodeVisitor.output_reference( childnode.attributes["refuri"], childnode.children[0].astext() ) else: self.output += childnode.astext() def visit_raw(self, node): theraw = " " + node.astext() self.output += ( "\n\n.. raw:: html\n\n \n" + " ".join(theraw.splitlines(True)) + "\n \n\n" ) def main(): parser = argparse.ArgumentParser( description="Change Log Generator", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "token", type=str, help="Enter GitHub Access Token (from https://github.com/" + "settings/tokens/new?description=GitHub%20Changelog%20Generator%20token)", ) args = parser.parse_args() doctree = changelog.compute_changelog(args.token, option_spec) doc = du.new_document("foo") doc.insert(0, doctree) nv = NodeVisitor(doc) for item in doctree: item.walk(nv) finaloutput = "=========\nChangelog\n=========\n\n" + nv.output with open(OUTPUT_FILENAME, "w") as text_file: text_file.write(finaloutput) if __name__ == "__main__": main() python-ring-doorbell-0.8.7/scripts/ringcli.py000077500000000000000000000007431456036304300213070ustar00rootroot00000000000000#!/usr/bin/env python # vim:sw=4:ts=4:et # Many thanks to @troopermax """DEPRACATED script which just runs the cli videos subcommand. Deprecated as you can either call the cli directly: ring_doorbell/cli.py videos Or use the ring-doorbell script if you installed ring_doorbell via poetry or pip ring-doorbell videos """ import sys from ring_doorbell.cli import cli if __name__ == "__main__": sys.argv.insert(1, "videos") sys.exit(cli()) python-ring-doorbell-0.8.7/test.py000066400000000000000000000004541456036304300171440ustar00rootroot00000000000000"""Test module which just runs the cli. Deprecated as you can either call the cli directly: python3 ring_doorbell/cli.py Or use the ring-doorbell script if you installed ring_doorbell via poetry or pip """ import sys from ring_doorbell.cli import cli if __name__ == "__main__": sys.exit(cli()) python-ring-doorbell-0.8.7/tests/000077500000000000000000000000001456036304300167525ustar00rootroot00000000000000python-ring-doorbell-0.8.7/tests/__init__.py000066400000000000000000000000531456036304300210610ustar00rootroot00000000000000"""Tests for Ring Door Bell components.""" python-ring-doorbell-0.8.7/tests/conftest.py000066400000000000000000000126461456036304300211620ustar00rootroot00000000000000"""Test configuration for the Ring platform.""" import json import os import re 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): dings = json.loads(active_dings) for ding in dings: ding["now"] = time() return json.dumps(dings) def load_fixture(filename): """Load a fixture.""" path = os.path.join(os.path.dirname(__file__), "fixtures", filename) with open(path) as fdp: return fdp.read() @pytest.fixture(autouse=True) def listen_mock(mocker, request): 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/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"), # "https://api.ring.com/clients_api/dings/987654321/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.7/tests/fixtures/000077500000000000000000000000001456036304300206235ustar00rootroot00000000000000python-ring-doorbell-0.8.7/tests/fixtures/ring_chime_health_attrs.json000066400000000000000000000010661456036304300263670ustar00rootroot00000000000000{ "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.7/tests/fixtures/ring_devices.json000066400000000000000000000353351456036304300241700ustar00rootroot00000000000000{ "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 } } ] }python-ring-doorbell-0.8.7/tests/fixtures/ring_devices_updated.json000066400000000000000000000243441456036304300256740ustar00rootroot00000000000000{ "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.7/tests/fixtures/ring_ding_active.json000066400000000000000000000046231456036304300250160ustar00rootroot00000000000000[ { "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.7/tests/fixtures/ring_doorboot_health_attrs.json000066400000000000000000000010661456036304300271310ustar00rootroot00000000000000{ "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.7/tests/fixtures/ring_doorboot_health_attrs_id987653.json000066400000000000000000000010661456036304300303130ustar00rootroot00000000000000{ "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.7/tests/fixtures/ring_doorbot_history.json000066400000000000000000000012301456036304300257620ustar00rootroot00000000000000[{ "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.7/tests/fixtures/ring_group_devices.json000066400000000000000000000013471456036304300254000ustar00rootroot00000000000000{ "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.7/tests/fixtures/ring_groups.json000066400000000000000000000014221456036304300240530ustar00rootroot00000000000000{ "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.7/tests/fixtures/ring_intercom_settings.json000066400000000000000000000245521456036304300263050ustar00rootroot00000000000000{ "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.7/tests/fixtures/ring_intercom_users.json000066400000000000000000000012251456036304300255760ustar00rootroot00000000000000[ { "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.7/tests/fixtures/ring_listen_credentials.json000066400000000000000000000005331456036304300264110ustar00rootroot00000000000000{ "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.7/tests/fixtures/ring_listen_ding.json000066400000000000000000000015021456036304300250320ustar00rootroot00000000000000{ "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.7/tests/fixtures/ring_listen_fcmdata.json000066400000000000000000000002651456036304300255150ustar00rootroot00000000000000{ "data": { "gcmData": "ring_listen_gcmdata.json" }, "from": "123456789012", "priority": "high", "fcmMessageId": "1234fdeb-1234-bc78-cd12-abcdef123456" }python-ring-doorbell-0.8.7/tests/fixtures/ring_listen_motion.json000066400000000000000000000017121456036304300254210ustar00rootroot00000000000000{ "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.7/tests/fixtures/ring_oauth.json000066400000000000000000000003111456036304300236500ustar00rootroot00000000000000{ "access_token": "eyJ0eWfvEQwqfJNKyQ9999", "token_type": "bearer", "expires_in": 3600, "refresh_token": "67695a26bdefc1ac8999", "scope": "client", "created_at": 1529099870 } python-ring-doorbell-0.8.7/tests/fixtures/ring_session.json000066400000000000000000000023401456036304300242170ustar00rootroot00000000000000{ "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.7/tests/test_cli.py000066400000000000000000000173221456036304300211370ustar00rootroot00000000000000import json import os from pathlib import Path 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.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)\nDownstairs (chime)\nFront (hp_cam_v1)\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("builtins.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, *args, **kwargs): nonlocal call_count, exception if call_count == 0: call_count += 1 raise exception("Simulated exception") 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): # mocker.patch("firebase_messaging.checkin", return_value="foobar") 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 os.path.isfile("credentials.json") await runner.invoke(listen, ["--store-credentials"], obj=ring) assert os.path.isfile("credentials.json") 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): # mocker.patch("firebase_messaging.checkin", return_value="foobar") 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.7/tests/test_listen.py000066400000000000000000000152101456036304300216600ustar00rootroot00000000000000"""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() ring.update_dings() 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" 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" 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() @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.7/tests/test_other.py000066400000000000000000000066411456036304300215130ustar00rootroot00000000000000"""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 False 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 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.7/tests/test_ring.py000066400000000000000000000122021456036304300213170ustar00rootroot00000000000000"""The tests for the Ring platform.""" 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 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 assert group.has_capability("something-else") is False 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 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.7/tox.ini000066400000000000000000000013111456036304300171170ustar00rootroot00000000000000[tox] envlist = py38, py39, py310, py311, py312, lint, docs skip_missing_interpreters = True isolated_build = true [testenv] allowlist_externals = poetry commands_pre = poetry install --no-root --sync --extras listen commands = poetry run pytest tests/ --cov=ring_doorbell --cov-report=xml --cov-report=term-missing [testenv:lint] allowlist_externals = poetry commands_pre = poetry install --no-root --sync --extras listen ignore_errors = True commands = poetry run pre-commit run --all-files [testenv:docs] allowlist_externals = poetry make commands_pre = poetry install --no-root --sync --extras docs --without dev ignore_errors = True commands = poetry run make -C docs html