pax_global_header00006660000000000000000000000064147213527320014520gustar00rootroot0000000000000052 comment=8aaba5962f5529834321fd14338d53bb3241bdd2 python-ring-doorbell-0.9.13/000077500000000000000000000000001472135273200156705ustar00rootroot00000000000000python-ring-doorbell-0.9.13/.github/000077500000000000000000000000001472135273200172305ustar00rootroot00000000000000python-ring-doorbell-0.9.13/.github/actions/000077500000000000000000000000001472135273200206705ustar00rootroot00000000000000python-ring-doorbell-0.9.13/.github/actions/setup/000077500000000000000000000000001472135273200220305ustar00rootroot00000000000000python-ring-doorbell-0.9.13/.github/actions/setup/action.yml000066400000000000000000000026501472135273200240330ustar00rootroot00000000000000--- name: Setup Environment description: Install uv, configure the system python, and the package dependencies inputs: uv-install-options: default: "" uv-version: default: 0.4.16 python-version: required: true cache-pre-commit: default: false cache-version: default: "v0.1" runs: using: composite steps: - name: Install uv uses: astral-sh/setup-uv@v3 with: enable-cache: true version: "${{ inputs.uv-version }}" - name: "Setup python" uses: "actions/setup-python@v5" id: setup-python with: python-version: "${{ inputs.python-version }}" allow-prereleases: true - name: "Install project" shell: bash run: | uv sync ${{ inputs.uv-install-options }} - name: Read pre-commit version if: inputs.cache-pre-commit == 'true' id: pre-commit-version shell: bash run: >- echo "pre-commit-version=$(uv run pre-commit -V | awk '{print $2}')" >> $GITHUB_OUTPUT - uses: actions/cache@v4 if: inputs.cache-pre-commit == 'true' name: Pre-commit cache with: path: ~/.cache/pre-commit/ key: cache-${{ inputs.cache-version }}-${{ runner.os }}-${{ runner.arch }}-pre-commit-${{ steps.pre-commit-version.outputs.pre-commit-version }}-python-${{ inputs.python-version }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} python-ring-doorbell-0.9.13/.github/release-drafter.yml000066400000000000000000000000541472135273200230170ustar00rootroot00000000000000template: | ## What's Changed $CHANGES python-ring-doorbell-0.9.13/.github/workflows/000077500000000000000000000000001472135273200212655ustar00rootroot00000000000000python-ring-doorbell-0.9.13/.github/workflows/ci.yml000066400000000000000000000051601472135273200224050ustar00rootroot00000000000000name: CI on: push: branches: ["master"] pull_request: branches: ["master"] env: UV_VERSION: 0.4.17 PACKAGE_NAME: ring_doorbell jobs: linting: name: "Perform linting checks" runs-on: ubuntu-latest strategy: matrix: python-version: ["3.12"] steps: - name: "Checkout source files" uses: "actions/checkout@v4" - name: Setup environment uses: ./.github/actions/setup with: python-version: ${{ matrix.python-version }} uv-version: ${{ env.UV_VERSION }} uv-install-options: "--all-extras" cache-pre-commit: true - name: "Run pre-commit checks" run: | uv run pre-commit run --all-files --verbose docs: name: "Build docs" needs: linting runs-on: ubuntu-latest strategy: matrix: python-version: ["3.12"] steps: - uses: "actions/checkout@v4" - name: Setup environment uses: ./.github/actions/setup with: python-version: ${{ matrix.python-version }} uv-version: ${{ env.UV_VERSION }} uv-install-options: "--extra docs --no-dev" - name: Make docs uv run: | uv run make -C docs html tests: name: Tests - Python ${{ matrix.python-version}} on ${{ matrix.os }} needs: linting runs-on: ubuntu-latest strategy: fail-fast: true matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.9", "pypy-3.10"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: # Exclude pypy on windows due to performance issues - os: windows-latest python-version: "pypy-3.9" - os: windows-latest python-version: "pypy-3.10" steps: - uses: "actions/checkout@v4" - name: Setup environment uses: ./.github/actions/setup with: python-version: ${{ matrix.python-version }} uv-version: ${{ env.UV_VERSION }} - name: Run tests run: > uv run pytest tests/ --cov=${{ env.PACKAGE_NAME }} --cov-report=xml --cov-report=term-missing --import-mode importlib - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2.2.3 with: file: coverage.xml debug: true parallel: true if: ${{ success() && matrix.python-version == '3.12' }} finish: name: Finish coverage build needs: tests runs-on: ubuntu-latest steps: - name: Close parallel build uses: coverallsapp/github-action@v2.2.3 with: parallel-finished: true python-ring-doorbell-0.9.13/.github/workflows/locks-threads.yml000066400000000000000000000006541472135273200245600ustar00rootroot00000000000000name: Lock # yamllint disable-line rule:truthy on: schedule: - cron: "0 1 * * *" jobs: lock: if: github.repository_owner == 'python-ring-doorbell' runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v5.0.1 with: github-token: ${{ github.token }} issue-inactive-days: "30" issue-lock-reason: "" pr-inactive-days: "7" pr-lock-reason: "" python-ring-doorbell-0.9.13/.github/workflows/publish.yml000066400000000000000000000072101472135273200234560ustar00rootroot00000000000000name: Publish Python distribution to PyPI and TestPyPI on: push: branches: ["master"] tags: - '*' env: UV_VERSION: 0.4.17 PYPI_PROJECT: ring-doorbell PYTHON_VERSION: 3.12 # GITHUB_TOKEN must have write access # https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/signing-the-distribution-packages jobs: build: name: Build distribution runs-on: ubuntu-latest steps: - name: Checkout source files uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v3 with: version: ${{ env.UV_VERSION }} - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Build with uv run: uv build - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ publish-to-pypi: name: >- Publish to PyPI if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes needs: - build runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/${{ env.PYPI_PROJECT }} permissions: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 github-release: name: >- Create github release needs: - publish-to-pypi runs-on: ubuntu-latest permissions: contents: write # IMPORTANT: mandatory for making GitHub Releases id-token: write # IMPORTANT: mandatory for sigstore steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Sign the dists with Sigstore uses: sigstore/gh-action-sigstore-python@v2.1.1 with: inputs: >- ./dist/*.tar.gz ./dist/*.whl - name: Create GitHub Release env: GITHUB_TOKEN: ${{ github.token }} # Repo clone is required for --notes-from-tag to work run: | gh repo clone '${{ github.repository }}' cd ${{ github.event.repository.name }} gh release create '${{ github.ref_name }}' --verify-tag --notes-from-tag --title '${{ github.ref_name }}' ${{ contains(github.ref_name, 'dev') && '--prerelease --latest=false' || '--latest=true' }} cd .. - name: Upload artifact signatures to GitHub Release env: GITHUB_TOKEN: ${{ github.token }} # Upload to GitHub Release using the `gh` CLI. # `dist/` contains the built packages, and the # sigstore-produced signatures and certificates. run: >- gh release upload '${{ github.ref_name }}' dist/** --repo '${{ github.repository }}' publish-to-testpypi: name: Publish to TestPyPI needs: - build runs-on: ubuntu-latest environment: name: testpypi url: https://test.pypi.org/p/${{ env.PYPI_PROJECT }} permissions: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ skip-existing: true python-ring-doorbell-0.9.13/.github/workflows/stale.yml000066400000000000000000000052071472135273200231240ustar00rootroot00000000000000name: Stale # yamllint disable-line rule:truthy on: schedule: - cron: "0 0 * * *" workflow_dispatch: jobs: stale: if: github.repository_owner == 'python-ring-doorbell' runs-on: ubuntu-latest steps: - name: Stale issues and prs uses: actions/stale@v9.0.0 with: repo-token: ${{ github.token }} days-before-stale: 90 days-before-close: 7 operations-per-run: 250 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no-stale,help-wanted,needs-more-information,waiting-for-reporter" stale-pr-label: "stale" exempt-pr-labels: "no-stale" stale-pr-message: > There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. If you are the author of this PR, please leave a comment if you want to keep it open. Also, please rebase your PR onto the latest dev branch to ensure that it's up to date with the latest changes. Thank you for your contribution! stale-issue-message: > There hasn't been any activity on this issue recently. This issue has been automatically marked as stale because of that. It will be closed if no further activity occurs. Please make sure to update to the latest ring_doorbell version and check if that solves the issue. Thank you for your contributions. - name: Needs-more-information and waiting-for-reporter stale issues policy uses: actions/stale@v9.0.0 with: repo-token: ${{ github.token }} only-labels: "needs-more-information,waiting-for-reporter" days-before-stale: 21 days-before-close: 7 days-before-pr-stale: -1 days-before-pr-close: -1 operations-per-run: 250 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no-stale,help-wanted" stale-issue-message: > There hasn't been any activity on this issue recently and it has been waiting for the reporter to provide information or an update. This issue has been automatically marked as stale because of that. It will be closed if no further activity occurs. Please make sure to update to the latest ring_doorbell version and check if that solves the issue. Thank you for your contributions. python-ring-doorbell-0.9.13/.github_changelog_generator000066400000000000000000000010341472135273200232260ustar00rootroot00000000000000output=CHANGELOG.md user=python-ring-doorbell project=python-ring-doorbell release-branch=master usernames-as-github-logins=true add-sections={"new-device":{"prefix":"**Added support for devices:**","labels":["new device"]},"docs":{"prefix":"**Documentation updates:**","labels":["documentation"]},"maintenance":{"prefix":"**Project maintenance:**","labels":["maintenance"]}} exclude-labels=duplicate,question,invalid,wontfix,release-prep,stale,bug issues-wo-labels=false bug-labels=bugfix breaking-label=**Breaking change pull requests:** python-ring-doorbell-0.9.13/.gitignore000066400000000000000000000014731472135273200176650ustar00rootroot00000000000000# ---> Python # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Visual Studio .vs # Visual Studio Code .vscode # Distribution / packaging .Python env/ venv/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ *.cache credentials.json *.code-workspace python-ring-doorbell-0.9.13/.hound.yml000066400000000000000000000000311472135273200176000ustar00rootroot00000000000000python: enabled: false python-ring-doorbell-0.9.13/.pre-commit-config.yaml000066400000000000000000000021651472135273200221550ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. rev: 0.4.17 hooks: # Update the uv lockfile - id: uv-lock - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-docstring-first - id: check-yaml - id: debug-statements - id: check-ast - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/PyCQA/doc8 rev: 'v1.1.1' hooks: - id: doc8 additional_dependencies: [tomli] - repo: local hooks: # Run mypy in the virtual environment so it uses the installed dependencies # for more accurate checking than using the pre-commit mypy mirror - id: mypy name: mypy entry: uv run mypy language: system types_or: [python, pyi] require_serial: true exclude: | # exclude required because --all-files passes py and pyi (?x)^( scripts/.*| docs/.*| tests/.*| test\.py$ | test_sync\.py$ )$ python-ring-doorbell-0.9.13/.readthedocs.yaml000066400000000000000000000021751472135273200211240ustar00rootroot00000000000000# 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: - docs python-ring-doorbell-0.9.13/CHANGELOG.md000066400000000000000000001221741472135273200175100ustar00rootroot00000000000000# Changelog ## [0.9.13](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.13) (2024-11-26) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.12...0.9.13) **Release highlights:** Support for the new 2024 Battery Doorbell. Many thanks @dan5py! **Merged pull requests:** - Support new Battery Doorbell device [\#474](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/474) (@dan5py) ## [0.9.12](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.12) (2024-11-12) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.9...0.9.12) **Release highlights:** - Fix for Ring Elite motion and ding events - Add `is_update` to ring events to enable filtering duplicates **Implemented enhancements:** - Add is\_update flag to ring events for updated events [\#467](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/467) (@sdb9696) **Fixed bugs:** - Add new push notification intercom unlock [\#464](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/464) (@sdb9696) - Fix doorbell elite missing capabilities [\#463](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/463) (@sdb9696) **Project maintenance:** - Update websockets dependency for \>=13 [\#469](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/469) (@sdb9696) ## [0.9.9](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.9) (2024-11-06) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.8...0.9.9) **Release highlights:** - Support for asynchronous webrtc offers with trickle ice candidates **Implemented enhancements:** - Enable async webrtc offers with trickle ICE candidates [\#460](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/460) (@sdb9696) ## [0.9.8](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.8) (2024-10-18) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.7...0.9.8) **Release highlights:** - **Breaking** - Devices no longer automatically refresh after calling setters. Consumers should call `async_refresh_devices` to get updated values. This allows consumers to wait for the api to properly reflect changes before reporting them back. - Fixes floodcam light switches - Ring servers suddenly require `PUT` requests to provide null json values. **Breaking change pull requests:** - Do not refresh devices after calling setters [\#454](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/454) (@sdb9696) **Implemented enhancements:** - Add light command to cli [\#457](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/457) (@sdb9696) **Fixed bugs:** - Fix 422 errors on PUT requests [\#456](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/456) (@sdb9696) - Add hardware\_id to all requests [\#455](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/455) (@sdb9696) ## [0.9.7](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.7) (2024-10-04) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.6...0.9.7) **Release highlights:** - Type checking will now raise errors when using deprecated attributes **Breaking change pull requests:** - Do not expose deprecated attributes to type checkers [\#451](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/451) (@sdb9696) **Implemented enhancements:** - Enable keep alive for webrtc stream [\#450](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/450) (@sdb9696) **Documentation updates:** - Update README.rst [\#444](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/444) (@jamesflores) **Project maintenance:** - Migrate workflows to setup-uv github action [\#449](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/449) (@sdb9696) ## [0.9.6](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.6) (2024-09-26) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.5...0.9.6) **Release highlights:** - Fix for a critical issue due to the ring authorisation api changing. All previous versions of this library will no longer work. **Implemented enhancements:** - Event listener capability enabled by default [\#445](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/445) (@sdb9696) **Fixed bugs:** - Send client\_id with oauth fetch tokens request [\#446](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/446) (@sdb9696) ## [0.9.5](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.5) (2024-09-19) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.4...0.9.5) **Release highlights:** - New CLI commands - Enhancement to the experimental WebRTC stream feature **Implemented enhancements:** - Enable multiple webrtc sessions per device [\#440](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/440) (@sdb9696) - Add cli command to open door on intercom [\#438](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/438) (@sdb9696) - Add in-home chime support to CLI [\#427](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/427) (@briangoldstein) **Fixed bugs:** - Fix max. volume of Ring Chime device. [\#439](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/439) (@daniel-k) - Fix cli listen command on windows [\#437](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/437) (@sdb9696) **Project maintenance:** - Fix testpypi publish workflow to skip duplicates [\#441](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/441) (@sdb9696) - Tweak the CI to use variables for project names [\#435](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/435) (@sdb9696) - Fix publish workflow action [\#434](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/434) (@sdb9696) - Upgrade artifact upload/download github actions [\#433](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/433) (@sdb9696) **Closed issues:** - pyproject include = \["LICENSE", "CONTRIBUTING.rst"...\] [\#324](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/324) ## [0.9.4](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.4) (2024-09-05) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.3...0.9.4) **Release highlights:** - Migrate from poetry to uv for package management - Bugfixes for in-home-chime **Implemented enhancements:** - Add WebRTC live streaming session generation [\#348](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/348) (@sdb9696) **Fixed bugs:** - Fix in-home chime duration setter [\#428](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/428) (@briangoldstein) **Documentation updates:** - Fix broken links in readme [\#426](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/426) (@briangoldstein) **Project maintenance:** - Migrate to uv and add testpypi publishing [\#430](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/430) (@sdb9696) ## [0.9.3](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.3) (2024-09-02) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.2...0.9.3) **Release highlights:** - The python-ring-doorbell code repository has moved to https://github.com/python-ring-doorbell/python-ring-doorbell - Fix for enabling in-home chimes - Many thanks @briangoldstein! **Fixed bugs:** - Fix active listen alert counter [\#423](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/423) (@sdb9696) - Fix method to enable in-home doorbell chime [\#419](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/419) (@briangoldstein) **Documentation updates:** - Update supported python version in readme [\#422](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/422) (@sdb9696) **Project maintenance:** - Migrate repo to python-ring-doorbell github organisation [\#421](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/421) (@sdb9696) - Remove anyio from dependencies [\#420](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/420) (@dotlambda) ## [0.9.2](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.2) (2024-08-29) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.1...0.9.2) **Release highlights:** - Fixes the broken event listener by migrating to the new `firebase-messaging` library to support FCM HTTP v1 api - **breaking** - the `RingEventListener` will only support async queries. Hence `start` and `stop` are now async defs **Fixed bugs:** - Fix event listener [\#416](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/416) (@sdb9696) ## [0.9.1](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.1) (2024-08-23) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.9.0...0.9.1) Hotfix for missing typing_extensions dependency **Fixed bugs:** - Fix missing typing\_extensions dependency [\#413](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/413) (@sdb9696) **Project maintenance:** - Update contributing docs to remove tox step [\#411](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/411) (@sdb9696) - Update and add code checkers [\#410](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/410) (@sdb9696) ## [0.9.0](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.9.0) (2024-08-21) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.12...0.9.0) **Release highlights:** - Async support for the library - Removed python 3.8 support **Breaking changes:** - Synchronous api calls are now deprecated. For example calling `Ring.update_data()` will emit a deprecation warning to use `await Ring.async_update_data()` which should be run within an event loop via `asyncio.run()`. See `test.py` for an example. - Calling the deprecated sync api methods from inside a running event loop is not supported. This is unlikely to affect many consumers as the norm if running in an event loop is to make synchronous api calls from an executor thread. - Python 3.8 is no longer officially supported and could break in future releases. **Breaking change pull requests:** - Drop python3.8 support and enable python3.13 in the CI [\#398](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/398) (@sdb9696) **Implemented enhancements:** - Make library fully async [\#361](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/361) (@sdb9696) **Fixed bugs:** - Small change to modify the timestamp [\#378](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/378) (@AndrewMohawk) **Project maintenance:** - Update instructions for releasing and migrate changelog [\#407](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/407) (@sdb9696) - Add .vscode folder to gitignore [\#397](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/397) (@sdb9696) - Update dependencies [\#396](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/396) (@sdb9696) - Reduce lock and stale workflow frequency [\#388](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/388) (@sdb9696) ## [0.8.12](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.12) (2024-06-27) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.11...0.8.12) **Merged pull requests:** - Fix license value in pyproject.toml for better compliance with accepted values [\#386](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/386) (@joostlek) - Fix stale workflow exclude list [\#377](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/377) (@sdb9696) - Fix lock and stale workflows [\#376](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/376) (@sdb9696) - Add stale and lock github workflows [\#375](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/375) (@sdb9696) - Update dependencies in lock file and pre-commit [\#374](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/374) (@sdb9696) - Enable windows, macos and pypy in the CI [\#373](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/373) (@sdb9696) - Update CI to cache pipx poetry app install [\#372](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/372) (@sdb9696) ## [0.8.11](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.11) (2024-04-09) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.10...0.8.11) **Merged pull requests:** - Fix get\_device missing authorized doorbots [\#368](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/368) (@sdb9696) ## [0.8.10](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.10) (2024-04-04) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.9...0.8.10) **Release highlights:** - py.typed added to library for type checkers **Merged pull requests:** - Update RingDevices class for better typing support and add py.typed [\#366](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/366) (@sdb9696) - Enable more ruff rules [\#365](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/365) (@joostlek) - Bump ruff to 0.3.5 [\#364](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/364) (@joostlek) ## [0.8.9](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.9) (2024-04-02) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.8...0.8.9) **Merged pull requests:** - Fix issue with third party devices returned in the group other [\#362](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/362) (@sdb9696) - Save gcm credentials in the cli as default [\#360](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/360) (@sdb9696) - Add typing and mypy checking [\#359](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/359) (@sdb9696) - Fix readme example and add to test.py [\#358](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/358) (@sdb9696) - Update CI to use environment caches [\#355](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/355) (@sdb9696) ## [0.8.8](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.8) (2024-03-18) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.7...0.8.8) **Merged pull requests:** - Bump cryptography from 41.0.6 to 42.0.0 [\#343](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/343) (@dependabot[bot]) - Handle Intercom unlock event [\#341](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/341) (@sdb9696) - Add history to Ring Intercom [\#340](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/340) (@cosimomeli) ## [0.8.7](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.7) (2024-02-06) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.6...0.8.7) **Release highlights:** - Support for Ring Intercoms. Many thanks to @rautsch & @andrew-rinato for initial PRs and special thanks to @cosimomeli for getting this over the line! **Merged pull requests:** - Add history to has\_capability check [\#342](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/342) (@sdb9696) - Upgrade CI poetry version to 1.7.1 [\#338](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/338) (@sdb9696) - Fix changelog link [\#337](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/337) (@sdb9696) - Migrate to ruff [\#336](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/336) (@sdb9696) - Make changelog autogenerated as part of CI [\#335](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/335) (@sdb9696) - Fix coverage over-reporting by uploading xml report [\#333](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/333) (@sdb9696) - Use coveralls github action [\#332](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/332) (@sdb9696) - Updated Intercom Support \(2024\) [\#330](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/330) (@cosimomeli) - Bump jinja2 from 3.1.2 to 3.1.3 [\#327](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/327) (@dependabot[bot]) - Remove exec permissions of ring\_doorbell/cli.py [\#323](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/323) (@cpina) - Bump cryptography from 41.0.5 to 41.0.6 [\#313](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/313) (@dependabot[bot]) ## [0.8.6](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.6) (2024-01-25) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.5...0.8.6) **Breaking change:** - Breaking change to the listen subpackage api to allow the listener be configurable. **Merged pull requests:** - Allow ring listener to be configurable [\#329](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/329) (@sdb9696) - Thank note for Debian package [\#326](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/326) (@tchellomello) ## [0.8.5](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.5) (2023-12-21) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.4...0.8.5) **Merged pull requests:** - Fix history timeformat and bump to 0.8.5 [\#320](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/320) (@sdb9696) ## [0.8.4](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.4) (2023-12-12) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.3...0.8.4) **Merged pull requests:** - Add Spotlight Cam Pro and enable motion detection for Stickup Cam [\#316](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/316) (@sdb9696) ## [0.8.3](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.3) (2023-11-27) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.2...0.8.3) **Merged pull requests:** - Fix auth when token invalid & rename device\_id parameters [\#311](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/311) (@sdb9696) - fix typo in the documentation [\#284](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/284) (@ghost) ## [0.8.2](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.2) (2023-11-24) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.1...0.8.2) **Merged pull requests:** - Add ring devices and bump version to 0.8.2 [\#310](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/310) (@sdb9696) ## [0.8.1](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.1) (2023-11-15) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.8.0...0.8.1) **Merged pull requests:** - Update CI for python 3.12 [\#307](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/307) (@sdb9696) - Wrap more exceptions in RingError [\#306](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/306) (@sdb9696) ## [0.8.0](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.8.0) (2023-11-08) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.7.7...0.8.0) **Merged pull requests:** - Add custom exceptions and encapsulate oauth error handling [\#304](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/304) (@sdb9696) ## [0.7.7](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.7.7) (2023-10-31) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.7.6...0.7.7) **Merged pull requests:** - Improve stability and capabilities of realtime event listener [\#300](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/300) (@sdb9696) ## [0.7.6](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.7.6) (2023-10-25) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.7.5...0.7.6) **Merged pull requests:** - Fix anyio dependency preventing ha install [\#298](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/298) (@sdb9696) ## [0.7.5](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.7.5) (2023-10-25) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.7.4...0.7.5) **Merged pull requests:** - Add event listener for getting realtime dings [\#296](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/296) (@sdb9696) - Add cli commands: devices, groups, dings and history [\#293](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/293) (@sdb9696) - Add motion detection cli command and improve formatting of show command [\#292](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/292) (@sdb9696) - Add tests for cli and fix issues with videos [\#290](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/290) (@sdb9696) ## [0.7.4](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.7.4) (2023-09-27) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.7.3...0.7.4) **Merged pull requests:** - Fix and update cli [\#288](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/288) (@sdb9696) - Update to pyproject.toml, poetry, and update docs to use yaml config [\#287](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/287) (@sdb9696) ## [0.7.3](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.7.3) (2023-09-11) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.7.2...0.7.3) **Merged pull requests:** - 0.7.3 release [\#285](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/285) (@sdb9696) - Add motion detection enabled switch [\#282](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/282) (@sdb9696) - Fix ci to use up to date python versions and include pre-commit-config [\#281](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/281) (@sdb9696) - Add support for Floodlight Cam Pro [\#280](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/280) (@twasilczyk) ## [0.7.2](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.7.2) (2021-12-18) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.7.1...0.7.2) **Merged pull requests:** - Recognize cocoa\_floodlight as a floodlight kind [\#255](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/255) (@mwren) ## [0.7.1](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.7.1) (2021-08-26) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.7.0...0.7.1) **Merged pull requests:** - fix memory growth when calling url\_recording [\#253](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/253) (@prwood80) - \[dist\] Fix coveralls build issue [\#238](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/238) (@decompil3d) - \[dist\] Disable Travis now that GH Actions is setup [\#236](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/236) (@decompil3d) - get\_snapshot\(\) logic to be compliant with legacy [\#234](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/234) (@tchellomello) - \[dist\] Use GitHub Actions [\#233](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/233) (@decompil3d) - \[feat\] Add support for Light Groups [\#231](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/231) (@decompil3d) - fix: prevent multiple device entries for "Python" in the Ring app when using this library [\#228](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/228) (@riptidewave93) - Fix live streaming json [\#225](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/225) (@JoeDaddy7105) - Fix Build Errors [\#224](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/224) (@JoeDaddy7105) - Fixed RingDoorBell.get\_snapshot\(\) and added download [\#218](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/218) (@NSEvent) - Fix get snapshot based on comments [\#196](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/196) (@dshokouhi) - Return None if no battery installed [\#185](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/185) (@balloob) ## [0.7.0](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.7.0) (2021-02-05) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.6.2...0.7.0) ## [0.6.2](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.6.2) (2020-11-21) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.6.1...0.6.2) ## [0.6.1](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.6.1) (2020-09-28) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.6.0...0.6.1) **Merged pull requests:** - Add latest device kinds [\#207](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/207) (@jsetton) - Pushes new documentation to \(http://python-ring-doorbell.readthedocs.io/\) [\#194](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/194) (@tchellomello) - Drop python 2.7/3.5. Updated readme and test.py examples [\#192](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/192) (@steve-gombos) ## [0.6.0](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.6.0) (2020-01-14) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.5.0...0.6.0) **Major breaking change:** Ring APIs offer 1 endpoint with all device info. 1 with all health for doorbells etc. The API used to make a request from each device to the "all device" endpoint and fetch its own data. With the new approach we now just fetch the data once and each device will fetch that data. This significantly reduces the number of requests. See updated [test.py](https://github.com/tchellomello/python-ring-doorbell/blob/0.6.0/test.py) on usage. Changes: - Pass a user agent to the auth class to identify your project (at request from Ring) - For most updates, just call `ring.update_all()`. If you want health data (wifi stuff), call `device.update_health_data()` on each device - Renamed `device.id` -> `device.device_id`, `device.account_id` -> `device.id` to follow API naming. - Call `ring.update_all()` at least once before querying for devices - Querying devices now is a function `ring.devices()` instead of property `ring.devices` - Removed `ring.chimes`, `ring.doorbells`, `ring.stickup_cams` - Cleaned up tests with pytest fixtures - Run Black on code to silence hound. **Merged pull requests:** - Refactor data handling [\#184](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/184) (@balloob) ## [0.5.0](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.5.0) (2020-01-12) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.4.0...0.5.0) **Breaking Change:** The `Auth` class no longer takes an `otp_callback` but now takes an `otp_code`. It raises `MissingTokenError` if `otp_code` is required. See the [updated example](https://github.com/tchellomello/python-ring-doorbell/blob/261eaf96875e51fc266a5dbfc6198f8cbb8006e0/test.py). **Implemented enhancements:** - Removed otp\_callback [\#180](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/180) (@steve-gombos) **Merged pull requests:** - Increased timeout from 5 to 10 seconds [\#179](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/179) (@cyberjunky) ## [0.4.0](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.4.0) (2020-01-11) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.2.9...0.4.0) **Major breaking change:** This release is a major breaking change to clean up the auth and follow proper OAuth2. Big thanks to @steve-gombos for this. All authentication is now done inside `Auth`. The first time you need username, password and optionally an 2-factor auth callback function. After that you have a token and that can be used. The old cache file is no longer in use and can be removed. Example usage in `test.py`. **Implemented enhancements:** - Auth and ring class refactor [\#175](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/175) (@steve-gombos) - Implemented timeouts for HTTP requests methods [\#165](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/165) (@tchellomello) - Support for device model name property and has capability method [\#116](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/116) (@jsetton) **Merged pull requests:** - Blocked user agent temp fix [\#176](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/176) (@steve-gombos) - Fixed logic and simplified module imports [\#168](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/168) (@tchellomello) - Fixes for tchellomello/python-ring-doorbell\#162 [\#163](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/163) (@ZachBenz) - Make consistent requirements.txt and setup.py [\#158](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/158) (@tchellomello) - Fixed requirements.xt [\#155](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/155) (@tchellomello) - fix R1705: Unnecessary elif after return \(no-else-return\) [\#151](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/151) (@xernaj) - Fix for Issue \#146 [\#149](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/149) (@ZachBenz) - Fix/oauth fail due to blocked user agent [\#143](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/143) (@xernaj) - Add additional device kinds for new products [\#137](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/137) (@jsetton) - Add a couple of device kinds [\#135](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/135) (@dshokouhi) - Fixed pylint and test errors [\#115](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/115) (@tchellomello) - support of externally powered new stickup cam [\#109](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/109) (@steveww) - Add support for downloading snapshot from doorbell [\#108](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/108) (@MorganBulkeley) - Support for Spotlight Battery cameras with multiple battery bays [\#106](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/106) (@evanjd) ## [0.2.9](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.2.9) (2020-01-03) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.2.8...0.2.9) **Implemented enhancements:** - add timeout to requests [\#164](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/164) **Closed issues:** - 3000 DNS queries a minute [\#160](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/160) ## [0.2.8](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.2.8) (2019-12-27) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.2.6...0.2.8) ## [0.2.6](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.2.6) (2019-12-27) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.2.5...0.2.6) ## [0.2.5](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.2.5) (2019-12-20) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.2.3...0.2.5) ## [0.2.3](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.2.3) (2019-03-05) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.2.2...0.2.3) **Implemented enhancements:** - Feature Request: Add a model property to identify the different products [\#112](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/112) **Closed issues:** - MSG\_GENERIC\_FAIL [\#114](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/114) ## [0.2.2](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.2.2) (2018-10-29) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.2.1...0.2.2) ## [0.2.1](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.2.1) (2018-06-15) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.2.0...0.2.1) ## [0.2.0](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.2.0) (2018-05-16) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.1.9...0.2.0) **Closed issues:** - Push Notification Token [\#61](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/61) **Merged pull requests:** - only save token to disk if reuse session is true [\#81](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/81) (@andrewkress) ## [0.1.9](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.1.9) (2017-11-29) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.1.8...0.1.9) **Implemented enhancements:** - Create a generic update\(\) call which updates all devices under top-level Ring object [\#74](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/74) - Created generic update method for all devices on Ring top-parent object [\#75](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/75) (@tchellomello) ## [0.1.8](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.1.8) (2017-11-22) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.1.7...0.1.8) ## [0.1.7](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.1.7) (2017-11-14) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.1.6...0.1.7) **Implemented enhancements:** - Doorbell history does not return all events [\#63](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/63) - Allows `older_than` parameter to history\(\) method [\#69](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/69) (@tchellomello) **Merged pull requests:** - Update README.rst [\#66](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/66) (@ntalekt) ## [0.1.6](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.1.6) (2017-10-19) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.1.5...0.1.6) **Implemented enhancements:** - Add support to Stick Up cameras [\#38](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/38) - Added floodlight lights and siren support [\#58](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/58) (@jsetton) ## [0.1.5](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.1.5) (2017-10-17) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.1.4...0.1.5) **Implemented enhancements:** - Split source code into different files [\#54](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/54) - Fix \_\_init\_\_ methods [\#49](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/49) - How to get RSSI? [\#47](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/47) - House keeping: split source code into different files [\#55](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/55) (@tchellomello) - Refactored unittests [\#53](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/53) (@tchellomello) - Implemented health parameters reporting \(wifi, wifi\_rssi\) [\#50](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/50) (@tchellomello) **Merged pull requests:** - add wifi connection status property [\#48](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/48) (@keeth) - chime: Support playing motion test sound [\#46](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/46) (@vickyg3) - adds support for stickup & floodlight cams [\#44](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/44) (@jlippold) ## [0.1.4](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.1.4) (2017-04-30) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/v0.1.3...0.1.4) **Merged pull requests:** - 0.1.4 [\#42](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/42) (@tchellomello) ## [v0.1.3](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/v0.1.3) (2017-03-31) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.1.2...v0.1.3) ## [0.1.2](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.1.2) (2017-03-20) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.1.1...0.1.2) **Implemented enhancements:** - Feature request: Change Chime ring [\#19](https://github.com/python-ring-doorbell/python-ring-doorbell/issues/19) - Allows to filter history by event kind: 'motion', 'on\_demand', 'ding' [\#20](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/20) (@tchellomello) **Merged pull requests:** - Extended unittest coverage to check\_alerts [\#30](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/30) (@tchellomello) - Added new example [\#27](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/27) (@tchellomello) - Update README.rst [\#26](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/26) (@tchellomello) - Rebasing master from dev [\#25](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/25) (@tchellomello) - Added basic structure for docs [\#24](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/24) (@tchellomello) - Unittests [\#22](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/22) (@tchellomello) - Introduced check\_alerts\(\) method [\#17](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/17) (@tchellomello) ## [0.1.1](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.1.1) (2017-03-09) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.1.0...0.1.1) **Merged pull requests:** - v0.1.1 [\#18](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/18) (@tchellomello) ## [0.1.0](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.1.0) (2017-02-25) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.0.4...0.1.0) **Breaking change:** The code was refactored to allow to manipulate the objects in a better way. **Implemented enhancements:** - Refactored project to make it more Pythonish and transparent [\#14](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/14) (@tchellomello) ## [0.0.4](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.0.4) (2017-02-15) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.0.3...0.0.4) **Merged pull requests:** - 0.0.4 [\#12](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/12) (@tchellomello) ## [0.0.3](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.0.3) (2017-02-15) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.0.2...0.0.3) **Merged pull requests:** - Fixed metadata setup.py [\#11](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/11) (@tchellomello) - 0.0.3 [\#10](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/10) (@tchellomello) ## [0.0.2](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.0.2) (2017-02-15) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/0.0.1...0.0.2) ## [0.0.1](https://github.com/python-ring-doorbell/python-ring-doorbell/tree/0.0.1) (2017-02-12) [Full Changelog](https://github.com/python-ring-doorbell/python-ring-doorbell/compare/1f01b44074cb8d72ca40c83b896ea79768fde885...0.0.1) **Merged pull requests:** - Merging from dev [\#5](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/5) (@tchellomello) - Implemented travis, tox tests [\#4](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/4) (@tchellomello) - Refactored and updated documentation [\#2](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/2) (@tchellomello) - Make flake8 happy [\#1](https://github.com/python-ring-doorbell/python-ring-doorbell/pull/1) (@tchellomello) \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* python-ring-doorbell-0.9.13/CONTRIBUTING.rst000066400000000000000000000072211472135273200203330ustar00rootroot00000000000000============ 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/python-ring-doorbell/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/python-ring-doorbell/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 `uv `_ for dependency management. If you dont have uv installed you can install it with:: $ pipx install uv This installs uv in a virtual environment to isolate it from the rest of your system. Then to install `python-ring-doorbell`:: $ uv sync --all-extras uv will create a virtual environment for you and install all the requirements #. Create a branch for local development:: $ git checkout -b NAME-OF-YOUR-BUGFIX-OR-FEATURE Now you can make your changes locally. #. To make sure your changes will pass the CI install pre-commit:: $ pre-commit install You can check your changes prior to commit with:: $ pre-commit run # Runs against files added to staging $ pre-commit run --all-files # Runs against files not yet added to staging #. To test your changes:: $ uv run pytest #. 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 ---------------- UV ~~~~~~ Dependencies ^^^^^^^^^^^^ uv is very useful at managing virtual environments and ensuring that dependencies all match up for you. It manages this with the use of the `uv.lock` file which contains all the exact versions to be installed. This means that if you add any dependecies you should do it via:: $ uv add pypi_project_name rather than pip. This will update `pyproject.toml` and `uv.lock` accordingly. To uninstall a dependency:: $ uv remove pypi_project_name finally if you want to add a dependency for development only:: $ uv add --dev pypi_project_name Environments ^^^^^^^^^^^^ uv creates a virtual environment for the project in the .venv directory. You can activate the virtual environment with:: $ source .venv/bin/activate To exit the shell type ``deactivate``. However you don't **need** to activate the virtual environment and you can run any command without activating it by:: $ uv run SOME_COMMAND See `uv `_ for more info Documentation ^^^^^^^^^^^^^ To build the docs install with the docs extra:: $ uv sync --extra docs Then build:: $ uv run make -C html python-ring-doorbell-0.9.13/LICENSE000066400000000000000000000167441472135273200167110ustar00rootroot00000000000000 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.9.13/README.rst000066400000000000000000000234031472135273200173610ustar00rootroot00000000000000===================== 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/python-ring-doorbell/python-ring-doorbell/actions/workflows/ci.yml/badge.svg?branch=master :alt: Build Status :target: https://github.com/python-ring-doorbell/python-ring-doorbell/actions/workflows/ci.yml?branch=master .. image:: https://coveralls.io/repos/github/python-ring-doorbell/python-ring-doorbell/badge.svg?branch=master :alt: Coverage :target: https://coveralls.io/github/python-ring-doorbell/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 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/python-ring-doorbell/python-ring-doorbell@master 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 #. See or manage your doorbell in-home chime settings:: $ ring-doorbell in-home-chime --device-name "Front Door" $ ring-doorbell in-home-chime --device-name "Front Door" type Mechanical $ ring-doorbell in-home-chime --device-name "Front Door" enabled True $ ring-doorbell in-home-chime --device-name "Front Door" duration 5 #. Query a ring api url directly:: $ ring-doorbell raw-query --url /clients_api/dings/active #. Run ``ring-doorbell --help`` or ``ring-doorbell --help`` for full options Using the API ------------- The API has an async interface and a sync interface. All api calls starting `async` are asynchronous. This is the preferred method of interacting with the ring api and the sync versions are maintained for backwards compatability. *You cannot call sync api functions from inside a running event loop.* Initializing your Ring object +++++++++++++++++++++++++++++ This code example is in the `test.py `_ file. For the deprecated sync example see `test_sync.py `_. .. code-block:: python import getpass import asyncio import json from pathlib import Path from ring_doorbell import Auth, AuthenticationError, Requires2FAError, Ring user_agent = "YourProjectName-1.0" # Change this cache_file = Path(user_agent + ".token.cache") def token_updated(token): cache_file.write_text(json.dumps(token)) def otp_callback(): auth_code = input("2FA code: ") return auth_code async def do_auth(): username = input("Username: ") password = getpass.getpass("Password: ") auth = Auth(user_agent, None, token_updated) try: await auth.async_fetch_token(username, password) except Requires2FAError: await auth.async_fetch_token(username, password, otp_callback()) return auth async def main(): if cache_file.is_file(): # auth token is cached auth = Auth(user_agent, json.loads(cache_file.read_text()), token_updated) ring = Ring(auth) try: await ring.async_create_session() # auth token still valid except AuthenticationError: # auth token has expired auth = await do_auth() else: auth = await do_auth() # Get new auth token ring = Ring(auth) await ring.async_update_data() devices = ring.devices() pprint(devices.devices_combined) await auth.async_close() if __name__ == "__main__": asyncio.run(main()) Event Listener ++++++++++++++ .. code-block:: python event_listener = RingEventListener(ring, credentials, credentials_updated_callback) event_listener.add_notification_callback(_event_handler(ring).on_event) await event_listener.start() 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']): await dev.async_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) await dev.async_set_volume(5) print('Volume: %s' % dev.volume) # play dev test shound if dev.family == 'chimes': await dev.async_test_sound(kind = 'ding') await dev.async_test_sound(kind = 'motion') # turn on lights on floodlight cam if dev.family == 'stickup_cams' and dev.lights: await dev.async_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 await doorbell.async_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 = await doorbell.async_history(kind='motion') Downloading the last video triggered by a ding or motion event ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ .. code-block:: python devices = ring.devices() doorbell = devices['doorbots'][0] await doorbell.async_recording_download( await doorbell.async_history(limit=100, kind='ding')[0]['id'], filename='last_ding.mp4', override=True) Displaying the last video capture URL +++++++++++++++++++++++++++++++++++++ .. code-block:: python print(await doorbell.async_recording_url(await doorbell.async_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 await group.async_set_lights(True) # Turn off lights await group.async_set_lights(False) # Turn on lights for 30 seconds await group.async_set_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.9.13/RELEASING.md000066400000000000000000000073111472135273200175250ustar00rootroot00000000000000# Releasing ## Requirements * [github client](https://github.com/cli/cli#installation) * [gitchub_changelog_generator](https://github.com/github-changelog-generator) * [github access token](https://github.com/github-changelog-generator/github-changelog-generator#github-token) ## Export changelog token ```bash export CHANGELOG_GITHUB_TOKEN=token ``` ## Set release information ```bash export NEW_RELEASE=x.x.x ``` ## Normal releases from master ### Create a branch for the release ```bash git checkout master git fetch upstream master git rebase upstream/master git checkout -b release/$NEW_RELEASE ``` ### Update the version number ```bash sed -i "0,/version = /{s/version = .*/version = \"${NEW_RELEASE}\"/}" pyproject.toml ``` ### Update dependencies ```bash uv sync --all-extras uv lock --upgrade uv sync --all-extras ``` ### Run pre-commit and tests ```bash uv run pre-commit run --all-files uv run pytest ``` ### Create release summary (skip for dev releases) Write a short and understandable summary for the release. Can include images. #### Create $NEW_RELEASE milestone in github If not already created #### Create new issue linked to the milestone ```bash gh issue create --label "release-summary" --milestone $NEW_RELEASE --title "$NEW_RELEASE Release Summary" --body "**Release highlights:**" ``` You can exclude the --body option to get an interactive editor or go into the issue on github and edit there. #### Close the issue Either via github or: ```bash gh issue close ISSUE_NUMBER ``` ### Generate changelog Configuration settings are in `.github_changelog_generator` #### For pre-release EXCLUDE_TAGS will exclude all dev tags except for the current release dev tags. Regex should be something like this `^((?!0\.9\.0)(.*dev\d))+`. The first match group negative matches on the current release and the second matches on releases ending with dev. ```bash EXCLUDE_TAGS=${NEW_RELEASE%.dev*}; EXCLUDE_TAGS=${EXCLUDE_TAGS//"."/"\."}; EXCLUDE_TAGS="^((?!"$EXCLUDE_TAGS")(.*dev\d))+" echo "$EXCLUDE_TAGS" github_changelog_generator --future-release $NEW_RELEASE --exclude-tags-regex "$EXCLUDE_TAGS" ``` #### For production ```bash github_changelog_generator --future-release $NEW_RELEASE --exclude-tags-regex 'dev\d$' ``` You can ignore warnings about missing PR commits like below as these relate to PRs to branches other than master: ``` Warning: PR 29 merge commit was not found in the release branch or tagged git history and no rebased SHA comment was found ``` ### Export new release notes to variable ```bash export RELEASE_NOTES=$(grep -Poz '(?<=\# Changelog\n\n)(.|\n)+?(?=\#\#)' CHANGELOG.md | tr '\0' '\n' ) echo "$RELEASE_NOTES" # Check the output and copy paste if neccessary ``` ### Commit and push the changed files ```bash git commit --all --verbose -m "Prepare $NEW_RELEASE" git push upstream release/$NEW_RELEASE -u ``` ### Create a PR for the release, merge it, and re-fetch the master #### Create the PR ``` gh pr create --title "Prepare $NEW_RELEASE" --body "$RELEASE_NOTES" --label release-prep --base master ``` #### Merge the PR once the CI passes Create a squash commit and add the markdown from the PR description to the commit description. ```bash gh pr merge --squash --body "$RELEASE_NOTES" ``` ### Rebase local master ```bash git checkout master git fetch upstream master git rebase upstream/master ``` ### Create a release tag Note, add changelog release notes as the tag commit message so `gh release create --notes-from-tag` can be used to create a release draft. ```bash git tag --sign $NEW_RELEASE -m "$RELEASE_NOTES" # to create an unsigned tag replace --sign with --annotate git push upstream $NEW_RELEASE ``` ### Approve the release workflow This will automatically deploy to pypi python-ring-doorbell-0.9.13/_config.yml000066400000000000000000000000341472135273200200140ustar00rootroot00000000000000theme: jekyll-theme-minimal python-ring-doorbell-0.9.13/docs/000077500000000000000000000000001472135273200166205ustar00rootroot00000000000000python-ring-doorbell-0.9.13/docs/Makefile000066400000000000000000000011761472135273200202650ustar00rootroot00000000000000# 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.9.13/docs/make.bat000066400000000000000000000014441472135273200202300ustar00rootroot00000000000000@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.9.13/docs/ruff.toml000066400000000000000000000002131472135273200204530ustar00rootroot00000000000000# This extends our general Ruff rules specifically for docs extend = "../pyproject.toml" lint.extend-ignore = [ "D100", "D103", ] python-ring-doorbell-0.9.13/docs/source/000077500000000000000000000000001472135273200201205ustar00rootroot00000000000000python-ring-doorbell-0.9.13/docs/source/changelog.md000066400000000000000000000000401472135273200223630ustar00rootroot00000000000000:::{include} ../../CHANGELOG.md python-ring-doorbell-0.9.13/docs/source/conf.py000066400000000000000000000025001472135273200214140ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # noqa: INP001 # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html from __future__ import annotations from importlib.metadata import version as _version # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "python-ring-doorbell" copyright = "2023, Marcelo Moreira de Mello" # noqa: A001 author = "Marcelo Moreira de Mello" release = _version("ring_doorbell") version = _version("ring_doorbell") # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ "sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.viewcode", "sphinx.ext.todo", "myst_parser", ] myst_enable_extensions = [ "colon_fence", ] templates_path = ["_templates"] exclude_patterns: list[str] = [] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] master_doc = "index" python-ring-doorbell-0.9.13/docs/source/contributing.rst000066400000000000000000000000441472135273200233570ustar00rootroot00000000000000.. include:: ../../CONTRIBUTING.rst python-ring-doorbell-0.9.13/docs/source/index.rst000066400000000000000000000007201472135273200217600ustar00rootroot00000000000000.. 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.9.13/pyproject.toml000066400000000000000000000075251472135273200206150ustar00rootroot00000000000000[project] name = "ring-doorbell" version = "0.9.13" description = "A Python library to communicate with Ring Door Bell (https://ring.com/)" authors = [{ name = "python-ring-doorbell developers" }] license = { text="LGPL-3.0-or-later" } readme = "README.rst" requires-python = ">=3.9.0" dependencies = [ "oauthlib>=3.0.0,<4", "pytz>=2022.0", "asyncclick>=8.1.7.1", "aiohttp>=3", "aiofiles>=23", "typing-extensions>=4.12.2,<5.0", "async-timeout>=3.0.0", "websockets>=13.0.0", "firebase-messaging>=0.4.0", ] keywords = [ "ring", "door bell", "camera", "home automation", ] classifiers = [ "Environment :: Other Environment", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Home Automation", "Topic :: Software Development :: Libraries :: Python Modules" ] [project.urls] homepage = "https://github.com/python-ring-doorbell/python-ring-doorbell" repository = "https://github.com/python-ring-doorbell/python-ring-doorbell" documentation = "http://python-ring-doorbell.readthedocs.io/" "Bug Tracker" = "https://github.com/python-ring-doorbell/python-ring-doorbell/issues" [project.scripts] ring-doorbell = "ring_doorbell.cli:cli" [project.optional-dependencies] docs = ["sphinx<7.2.6", "sphinx-rtd-theme~=1.3", "myst-parser"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.sdist] include = [ "/ring_doorbell", "/tests", "/CHANGELOG.md", ] [tool.uv] dev-dependencies = [ "mock", "pre-commit", "pytest", "pytest-cov", "requests-mock", "pytest-asyncio", "pytest-mock", "pytest-socket", "ruff", "types-pytz>=2022.0", "pytest-freezer~=0.4", "types-oauthlib>=3.0.0,<4", "aioresponses~=0.7", "types-aiofiles>=23", "mypy~=1.0" ] [tool.pytest.ini_options] testpaths = [ "tests" ] norecursedirs = ".git" asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" 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 omit = ["ring_doorbell/rtcstream.py*"] [tool.ruff] target-version = "py39" [tool.ruff.lint] ignore = [ "ANN101", # Self... explanatory "ANN102", # cls... just as useless "ANN401", # Opinionated warning on disallowing dynamically typed expressions "ASYNC109", # Opinionated warning on not allowing timeout parameters in favour of asyncio.timeout "COM812", # Conflicts with other rules "D203", # Conflicts with other rules "D213", # Conflicts with other rules "D417", # False positives in some occasions "ISC001", # Conflicts with other rules "PLR2004", # Just annoying, not really useful "TRY003", # Long exception messages in custom exception classes ] select = ["ALL"] exclude = [ "ring_doorbell/cli.py", "test.py", "test_sync.py" ] [tool.ruff.lint.pydocstyle] convention = "pep257" [tool.mypy] exclude = [ 'tests/.*', # TOML literal string (single-quotes, no escaping necessary) 'docs/.*', 'test\.py$', 'test_sync\.py$', ] disallow_untyped_defs = true [[tool.mypy.overrides]] module = [ "ring_doorbell.cli" ] disallow_untyped_defs = false [tool.doc8] paths = ["docs"] ignore = ["D001"] ignore-path = ["docs/build"] ignore-path-errors = ["docs/source/index.rst;D000"] python-ring-doorbell-0.9.13/ring_doorbell/000077500000000000000000000000001472135273200205115ustar00rootroot00000000000000python-ring-doorbell-0.9.13/ring_doorbell/__init__.py000066400000000000000000000022521472135273200226230ustar00rootroot00000000000000"""Python Package for interacting with Ring devices.""" from importlib.metadata import version __version__ = version("ring_doorbell") from ring_doorbell.auth import Auth from ring_doorbell.chime import RingChime from ring_doorbell.const import RingCapability, RingEventKind from ring_doorbell.doorbot import RingDoorBell from ring_doorbell.event import RingEvent from ring_doorbell.exceptions import ( AuthenticationError, Requires2FAError, RingError, RingTimeout, ) from ring_doorbell.generic import RingGeneric from ring_doorbell.group import RingLightGroup from ring_doorbell.listen import RingEventListener, RingEventListenerConfig from ring_doorbell.other import RingOther from ring_doorbell.ring import Ring, RingDevices from ring_doorbell.stickup_cam import RingStickUpCam __all__ = [ "Ring", "Auth", "RingDevices", "RingChime", "RingCapability", "RingEventKind", "RingStickUpCam", "RingLightGroup", "RingDoorBell", "RingOther", "RingEvent", "RingEventListener", "RingEventListenerConfig", "RingError", "AuthenticationError", "Requires2FAError", "RingTimeout", "RingGeneric", "RingEvent", ] python-ring-doorbell-0.9.13/ring_doorbell/auth.py000066400000000000000000000221501472135273200220240ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Auth Class.""" from __future__ import annotations import uuid from asyncio import TimeoutError from functools import cached_property from json import loads as json_loads from typing import TYPE_CHECKING, Any, Callable, ClassVar from aiohttp import BasicAuth, ClientError, ClientResponseError, ClientSession from oauthlib.common import urldecode from oauthlib.oauth2 import ( LegacyApplicationClient, MissingTokenError, OAuth2Error, TokenExpiredError, ) from ring_doorbell.const import NAMESPACE_UUID, TIMEOUT, OAuth from ring_doorbell.exceptions import ( AuthenticationError, Requires2FAError, RingError, RingTimeout, ) from ring_doorbell.util import _DeprecatedSyncApiHandler class Auth: """A Python Auth class for Ring.""" def __init__( self, user_agent: str, token: dict[str, Any] | None = None, token_updater: Callable[[dict[str, Any]], None] | None = None, hardware_id: str | None = None, *, http_client_session: ClientSession | None = None, ) -> None: """Initialise the auth class. :type token: Optional[Dict[str, str]] :type token_updater: Optional[Callable[[str], None]] """ self.user_agent = user_agent if hardware_id: self.hardware_id = hardware_id else: # Generate a UUID that will stay the same # for this physical device to prevent # multiple auth entries in ring.com self.hardware_id = str( uuid.uuid5(uuid.UUID(NAMESPACE_UUID), str(uuid.getnode()) + user_agent) ) self.device_model = "ring-doorbell:" + user_agent self.token_updater = token_updater self._token: dict[str, Any] = token or {} self._local_session: ClientSession | None = None self.http_client_session = http_client_session self._oauth_client = LegacyApplicationClient( client_id=OAuth.CLIENT_ID, token=token ) self._auth = BasicAuth(OAuth.CLIENT_ID, "") @property def _session(self) -> ClientSession: if self.http_client_session: return self.http_client_session if self._local_session is None: self._local_session = ClientSession() return self._local_session async def async_fetch_token( self, username: str, password: str, otp_code: str | None = None ) -> dict[str, Any]: """Fetch initial token with username/password & 2FA. :type username: str :type password: str :type otp_code: str. """ headers = {"User-Agent": self.user_agent, "hardware_id": self.hardware_id} if otp_code: headers["2fa-support"] = "true" headers["2fa-code"] = otp_code try: body = self._oauth_client.prepare_request_body( username, password, scope=OAuth.SCOPE, include_client_id=True ) data = dict(urldecode(body)) resp = await self._session.request( "POST", OAuth.ENDPOINT, data=data, headers=headers, auth=self._auth, ) async with resp: text = await resp.text() self._token = self._oauth_client.parse_request_body_response( text, scope=OAuth.SCOPE ) 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(self._token) return self._token async def async_refresh_tokens(self) -> dict[str, Any]: """Refresh the auth tokens.""" try: headers = { "Accept": "application/json", "Content-Type": ("application/x-www-form-urlencoded;charset=UTF-8"), } body = self._oauth_client.prepare_refresh_body( refresh_token=self._token["refresh_token"] ) data = dict(urldecode(body)) resp = await self._session.request( "POST", OAuth.ENDPOINT, data=data, headers=headers, auth=self._auth ) async with resp: text = await resp.text() self._token = self._oauth_client.parse_request_body_response( text, scope=OAuth.SCOPE ) except OAuth2Error as ex: raise AuthenticationError(ex) from ex if self.token_updater is not None: self.token_updater(self._token) return self._token def get_hardware_id(self) -> str: """Get hardware ID.""" return self.hardware_id def get_device_model(self) -> str: """Get device model.""" return self.device_model async def async_close(self) -> None: """Close aiohttp session.""" session = self._local_session self._local_session = None if session: await session.close() class Response: """Class for returning responses.""" def __init__(self, content: bytes, status_code: int) -> None: """Initialise thhe repsonse class.""" self.content = content self.status_code = status_code @property def text(self) -> str: """Response as text.""" return self.content.decode() def json(self) -> Any: """Response as loaded json.""" return json_loads(self.text) async def async_query( # noqa: C901, PLR0913 self, url: str, method: str = "GET", extra_params: dict[str, Any] | None = None, data: bytes | None = None, json: dict[Any, Any] | None = None, timeout: float | None = None, *, raise_for_status: bool = True, ) -> Auth.Response: """Query data from Ring API.""" if timeout is None: timeout = TIMEOUT params = {} if extra_params: params.update(extra_params) kwargs: dict[str, Any] = { "params": params, "timeout": timeout, } headers = {"User-Agent": self.user_agent, "hardware_id": self.get_hardware_id()} # Ring servers started requiring a null json value for PUT requests in 2024-10 if json is not None or method == "PUT": kwargs["json"] = json headers["Content-Type"] = "application/json" try: try: url, headers, data = self._oauth_client.add_token( url, http_method=method, body=data, headers=headers, ) resp = await self._session.request( method, url, headers=headers, data=data, **kwargs ) except TokenExpiredError: self._token = await self.async_refresh_tokens() url, headers, data = self._oauth_client.add_token( url, http_method=method, body=data, headers=headers, ) resp = await self._session.request( method, url, headers=headers, data=data, **kwargs ) except AuthenticationError: raise # refresh_tokens will return this error if not valid except TimeoutError as ex: msg = f"Timeout error during query of url {url}: {ex}" raise RingTimeout(msg) from ex except ClientError as ex: msg = f"aiohttp Client error during query of url {url}: {ex}" raise RingError(msg) from ex except Exception as ex: msg = f"Unknown error during query of url {url}: {ex}" raise RingError(msg) from ex async with resp: if resp.status == 401: # Check whether there's an issue with the token grant self._token = await self.async_refresh_tokens() if raise_for_status: try: resp.raise_for_status() except ClientResponseError as ex: msg = ( f"HTTP error with status code {resp.status} " f"during query of url {url}: {ex}" ) raise RingError(msg) from ex response_data = await resp.read() return Auth.Response(response_data, resp.status) @cached_property def _dep_handler(self) -> _DeprecatedSyncApiHandler: return _DeprecatedSyncApiHandler(self) DEPRECATED_API_QUERIES: ClassVar = { "fetch_token", "refresh_tokens", "close", "query", } if not TYPE_CHECKING: def __getattr__(self, name: str) -> Any: """Get a deprecated attribute or raise an error.""" if name in self.DEPRECATED_API_QUERIES: return self._dep_handler.get_api_query(self, name) msg = f"{self.__class__.__name__} has no attribute {name!r}" raise AttributeError(msg) python-ring-doorbell-0.9.13/ring_doorbell/chime.py000066400000000000000000000066061472135273200221600ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Chime wrapper.""" from __future__ import annotations import logging from typing import Any, ClassVar from ring_doorbell.const import ( CHIME_KINDS, CHIME_PRO_KINDS, CHIME_TEST_SOUND_KINDS, CHIME_VOL_MAX, CHIME_VOL_MIN, CHIMES_ENDPOINT, HEALTH_CHIMES_ENDPOINT, LINKED_CHIMES_ENDPOINT, MSG_VOL_OUTBOUND, TESTSOUND_CHIME_ENDPOINT, RingCapability, RingEventKind, ) from ring_doorbell.exceptions import RingError from ring_doorbell.generic import RingGeneric _LOGGER = logging.getLogger(__name__) class RingChime(RingGeneric): """Implementation for Ring Chime.""" @property def family(self) -> str: """Return Ring device family type.""" return "chimes" async def async_update_health_data(self) -> None: """Update health attrs.""" resp = await self._ring.async_query( HEALTH_CHIMES_ENDPOINT.format(self.device_api_id) ) self._health_attrs = resp.json().get("device_health", {}) @property def model(self) -> str: """Return Ring device model name.""" if self.kind in CHIME_KINDS: return "Chime" if self.kind in CHIME_PRO_KINDS: return "Chime Pro" return "Unknown Chime" def has_capability(self, capability: RingCapability | str) -> bool: """Return if device has specific capability.""" capability = ( capability if isinstance(capability, RingCapability) else RingCapability.from_name(capability) ) return capability == RingCapability.VOLUME @property def volume(self) -> int: """Return the chime volume.""" return self._attrs["settings"].get("volume", 0) async def async_set_volume(self, value: int) -> None: """Set the chime volume.""" if not ((isinstance(value, int)) and (CHIME_VOL_MIN <= value <= CHIME_VOL_MAX)): raise RingError(MSG_VOL_OUTBOUND.format(CHIME_VOL_MIN, CHIME_VOL_MAX)) params = { "chime[description]": self.name, "chime[settings][volume]": str(value), } url = CHIMES_ENDPOINT.format(self.device_api_id) await self._ring.async_query(url, extra_params=params, method="PUT") async def async_get_linked_tree(self) -> dict[str, Any]: """Return doorbell data linked to chime.""" url = LINKED_CHIMES_ENDPOINT.format(self.device_api_id) resp = await self._ring.async_query(url) return resp.json() async def async_test_sound( self, kind: RingEventKind | str = RingEventKind.DING ) -> bool: """Play chime to test sound.""" kind_str = kind.value if isinstance(kind, RingEventKind) else kind if kind_str not in CHIME_TEST_SOUND_KINDS: return False url = TESTSOUND_CHIME_ENDPOINT.format(self.device_api_id) await self._ring.async_query( url, method="POST", extra_params={"kind": kind_str} ) return True DEPRECATED_API_QUERIES: ClassVar = { *RingGeneric.DEPRECATED_API_QUERIES, "update_health_data", "test_sound", } DEPRECATED_API_PROPERTY_GETTERS: ClassVar = { *RingGeneric.DEPRECATED_API_PROPERTY_GETTERS, "linked_tree", } DEPRECATED_API_PROPERTY_SETTERS: ClassVar = { *RingGeneric.DEPRECATED_API_PROPERTY_SETTERS, "volume", } python-ring-doorbell-0.9.13/ring_doorbell/cli.py000066400000000000000000000670651472135273200216500ustar00rootroot00000000000000# vim:sw=4:ts=4:et # Many thanks to @troopermax """Python Ring command line interface.""" from __future__ import annotations import asyncio import functools import getpass import json import logging import select import sys from contextlib import asynccontextmanager from datetime import datetime from pathlib import Path, PurePath from typing import Sequence, cast, TypeVar, NoReturn import asyncclick as click from ring_doorbell import ( Auth, AuthenticationError, Requires2FAError, Ring, RingDoorBell, RingEvent, RingGeneric, RingStickUpCam, RingOther, RingCapability, ) from ring_doorbell.const import ( CLI_TOKEN_FILE, GCM_TOKEN_FILE, PACKAGE_NAME, USER_AGENT, DOORBELL_EXISTING_TYPE, ) def _header() -> None: _bar() echo("Ring CLI") def _bar() -> None: echo("---------------------------------") def error(msg: str) -> NoReturn: """Print an error and exit.""" echo(msg) sys.exit(1) click.anyio_backend = "asyncio" # type: ignore[attr-defined] pass_ring = click.make_pass_decorator(Ring) pass_doorbell = click.make_pass_decorator(RingDoorBell) cache_file = Path(CLI_TOKEN_FILE) gcm_cache_file = Path(GCM_TOKEN_FILE) def CatchAllExceptions(cls): """Capture all exceptions and prints them nicely. Idea from https://stackoverflow.com/a/44347763 and https://stackoverflow.com/questions/52213375 """ def _handle_exception(debug, exc): if isinstance(exc, click.ClickException): raise # Handle exit request from click. if isinstance(exc, click.exceptions.Exit): sys.exit(exc.exit_code) echo(f"Raised error: {exc}") if debug: raise echo("Run with --debug enabled to see stacktrace") sys.exit(1) class _CommandCls(cls): _debug = False async def make_context(self, info_name, args, parent=None, **extra): self._debug = any( [arg for arg in args if arg in ["--debug", "-d", "--verbose", "-v"]] ) try: return await super().make_context( info_name, args, parent=parent, **extra ) except Exception as exc: _handle_exception(self._debug, exc) async def invoke(self, ctx): try: return await super().invoke(ctx) except asyncio.CancelledError: pass except KeyboardInterrupt: echo("Cli interrupted with keyboard interrupt") except Exception as exc: _handle_exception(self._debug, exc) return _CommandCls class MutuallyExclusiveOption(click.Option): """Prevents incompatable options being supplied, i.e. on and off.""" def __init__(self, *args, **kwargs) -> None: self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", [])) _help = kwargs.get("help", "") if self.mutually_exclusive: ex_str = ", ".join(self.mutually_exclusive) kwargs["help"] = _help + ( " NOTE: This argument is mutually exclusive with " " arguments: [" + ex_str + "]." ) super().__init__(*args, **kwargs) async def handle_parse_result(self, ctx, opts, args): if self.mutually_exclusive.intersection(opts) and self.name in opts: msg = ( "Illegal usage: `{}` is mutually exclusive with " "arguments `{}`.".format(self.name, ", ".join(self.mutually_exclusive)) ) raise click.UsageError(msg) return await super().handle_parse_result(ctx, opts, args) echo = click.echo def token_updated(token) -> None: """Writes token to file.""" cache_file.write_text(json.dumps(token), encoding="utf-8") def _format_filename(device_name, event): if not isinstance(event, dict): return None answered_status = "answered" if event["answered"] else "not_answered" filename = "{}_{}_{}_{}_{}".format( device_name, event["created_at"], event["kind"], answered_status, event["id"] ) return filename.replace(" ", "_").replace(":", ".") + ".mp4" async 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: await auth.async_fetch_token(username, password) return auth except Requires2FAError: await auth.async_fetch_token(username, password, input("2FA Code: ")) return auth async def _get_ring(username, password, do_update_data, user_agent=USER_AGENT): # connect to Ring account global cache_file, gcm_cache_file if user_agent != USER_AGENT: cache_file = Path(user_agent + ".token.cache") gcm_cache_file = Path(user_agent + ".gcm_token.cache") if cache_file.is_file(): auth = Auth( user_agent, json.loads(cache_file.read_text(encoding="utf-8")), token_updated, ) ring = Ring(auth) do_method = ( ring.async_update_data if do_update_data else ring.async_create_session ) try: await do_method() except AuthenticationError: auth = await _do_auth(username, password) ring = Ring(auth) do_method = ( ring.async_update_data if do_update_data else ring.async_create_session ) await do_method() else: auth = await _do_auth(username, password, user_agent=user_agent) ring = Ring(auth) do_method = ( ring.async_update_data if do_update_data else ring.async_create_session ) await do_method() return ring _T = TypeVar("_T") def _get_device( ring: Ring, device_families: list[str], device_type: type[_T], device_name: str | None = None, *, device_description: str | None = None, ) -> _T: description = ( device_description if device_description else " or ".join(device_families) ) if not device_name: dev_dict: dict[int, RingGeneric] = {} devices = ring.devices() for device_family in device_families: for dev in devices[device_family]: dev_dict[dev.device_api_id] = dev devs = list(dev_dict.values()) found = len(dev_dict) if found == 1: return cast(_T, devs[0]) elif found == 0: error(f"No {description} found") else: error( f"There are {found} {description}s, you need to pass the --device-name option." ) elif device := ring.get_device_by_name(device_name): if device.family in device_families and isinstance(device, device_type): return device else: error(f"{device_name} is not a {description}") else: error(f"Cannot find {description} with name {device_name}") @click.group( invoke_without_command=True, cls=CatchAllExceptions(click.Group), ) @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.""" # no need to perform any checks if we are just displaying the help if "--help" in sys.argv: # Context object is required to avoid crashing on sub-groups ctx.obj = Ring(None) return _header() logging.basicConfig() log_level = logging.DEBUG if debug else logging.INFO logger = logging.getLogger(PACKAGE_NAME) logger.setLevel(log_level) logger = logging.getLogger("firebase_messaging") logger.setLevel(log_level) no_update_commands = ["listen"] no_update = ctx.invoked_subcommand in no_update_commands @asynccontextmanager async def async_wrapped_ring(ring: Ring): try: yield ring finally: await ring.auth.async_close() ring = await _get_ring(username, password, not no_update, user_agent) # wrapped ring will ensure async_close is called when cli is finished ctx.obj = await ctx.with_async_resource(async_wrapped_ring(ring)) if ctx.invoked_subcommand is None: return await ctx.invoke(show) return None @cli.command(name="list") @pass_ring async def list_command(ring: Ring) -> None: """List ring devices.""" devices = ring.devices() device: RingGeneric | None = None for device in devices.doorbots: echo(device) for device in devices.authorized_doorbots: echo(device) for device in devices.chimes: echo(device) for device in devices.stickup_cams: echo(device) for device in devices.other: echo(device) @cli.command() @pass_ring @click.pass_context @click.option( "--on", "turn_on", cls=MutuallyExclusiveOption, is_flag=True, default=None, required=False, mutually_exclusive=["--off"], ) @click.option( "--off", "turn_off", cls=MutuallyExclusiveOption, is_flag=True, default=None, required=False, mutually_exclusive=["--on"], ) @click.option( "--device-name", "-dn", required=True, default=None, help="Name of the ring device", ) async def motion_detection(ctx, ring: Ring, device_name, turn_on, turn_off): """Get and change the motion detecton status of a device.""" device = ring.get_device_by_name(device_name) if not device: echo( f"No device with name {device_name} found." + " List of found device names (kind) is:" ) return await ctx.invoke(list_command) if not device.has_capability("motion_detection"): echo(f"{device!s} is not capable of motion detection") return None device = cast(RingDoorBell, device) state = "on" if device.motion_detection else "off" if not turn_on and not turn_off: echo(f"{device!s} has motion detection {state}") return None is_on = device.motion_detection if (turn_on and is_on) or (turn_off and not is_on): echo(f"{device!s} already has motion detection {state}") return None await device.async_set_motion_detection(turn_on if turn_on else False) await ring.async_update_devices() state = "on" if device.motion_detection else "off" echo(f"{device!s} motion detection set to {state}") return None @cli.command() @pass_ring @click.pass_context @click.argument("enable", type=click.BOOL, default=None, required=False) @click.option( "--device-name", "-dn", required=True, default=None, help="Name of the ring device", ) async def light(ctx, ring: Ring, device_name, enable): """Get and change the light state of a device.""" 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(RingCapability.LIGHT): echo(f"{device!s} does not have a light") return None device = cast(RingStickUpCam, device) state = "on" if device.light else "off" if enable is None: echo(f"{device!s} light is {state}") return None is_on = device.light if (enable and is_on) or (not enable and not is_on): echo(f"{device!s} light is already {state}") return None await device.async_set_light(enable) await ring.async_update_devices() state = "on" if device.light else "off" echo(f"{device!s} light set to {state}") return None @cli.command() @click.option( "--device-name", "-dn", required=False, default=None, help="Name of device, if ommited shows all devices", ) @pass_ring @click.pass_context async def show(ctx, ring: Ring, device_name): """Display ring devices.""" devices: Sequence[RingGeneric] | None = None if device_name and (device := ring.get_device_by_name(device_name)): devices = [device] elif device_name: echo( f"No device with name {device_name} found. " + "List of found device names (kind) is:" ) return await ctx.invoke(list_command) else: devices = ring.get_device_list() for dev in devices: await dev.async_update_health_data() echo("Name: %s" % dev.name) echo("Family: %s" % dev.family) echo("ID: %s" % dev.id) echo("Timezone: %s" % dev.timezone) echo("Wifi Name: %s" % dev.wifi_name) echo("Wifi RSSI: %s" % dev.wifi_signal_strength) echo() return None @cli.command(name="devices") @click.option( "--device-name", "-dn", required=False, default=None, help="Name of device, if ommited shows all devices", ) @click.option( "--json", "json_flag", required=False, is_flag=True, help="Output raw json", ) @pass_ring @click.pass_context async def devices_command(ctx, ring: Ring, device_name, json_flag): """Get device information.""" if not json_flag: echo( "(Pretty format coming soon, if you want json consistently " + "from this command provide the --json flag)" ) device_json = None if device_name and (device := ring.get_device_by_name(device_name)): device_json = ring.devices_data[device.family][device.id] elif device_name: echo( f"No device with name {device_name} found. " + "List of found device names (kind) is:" ) return await ctx.invoke(list_command) if device_json: echo(json.dumps(device_json, indent=2)) return None else: for device_type in ring.devices_data: for device_api_id in ring.devices_data[device_type]: echo( json.dumps(ring.devices_data[device_type][device_api_id], indent=2) ) return None @cli.command() @click.option( "--json", "json_flag", required=False, is_flag=True, help="Output raw json", ) @pass_ring async def dings(ring: Ring, json_flag) -> None: """Get dings information.""" if not json_flag: echo( "(Pretty format coming soon, if you want json consistently " + "from this command provide the --json flag)" ) echo(json.dumps(ring.dings_data, indent=2)) @cli.command() @click.option( "--json", "json_flag", required=False, is_flag=True, help="Output raw json", ) @pass_ring async def groups(ring: Ring, json_flag) -> None: """Get group information.""" if not json_flag: echo( "(Pretty format coming soon, if you want json consistently " + "from this command provide the --json flag)" ) if not ring.groups_data: echo("No ring device groups setup") else: for light_group in ring.groups().values(): await light_group.async_update() echo(json.dumps(light_group._attrs, indent=2)) echo(json.dumps(light_group._health_attrs, indent=2)) @cli.command() @click.option( "--url", required=True, type=str, help="Url to query, i.e. /clients_api/dings/active", ) @pass_ring async def raw_query(ring: Ring, url) -> None: """Directly query a url and return json result.""" resp = await ring.async_query(url) data = resp.json() echo(json.dumps(data, indent=2)) @cli.command(name="history") @click.option( "--device-name", "-dn", required=False, default=None, help="Name of device, if ommited shows all devices", ) @click.option( "--limit", required=False, default=5, help="Limit number of records to return", ) @click.option( "--kind", required=False, default=None, type=click.Choice(["ding", "motion", "on_demand"], case_sensitive=False), help="Get devices", ) @click.option( "--json", "json_flag", required=False, is_flag=True, help="Output raw json", ) @pass_ring @click.pass_context async def history_command(ctx, ring: Ring, device_name, kind, limit, json_flag): """Print raw json.""" if not json_flag: echo( "(Pretty format coming soon, if you want json consistently " + "from this command provide the --json flag)" ) device = ring.get_device_by_name(device_name) if not device: echo( f"No device with name {device_name} found. " + "List of found device names (kind) is:" ) return await ctx.invoke(list_command) history = await device.async_history(limit=limit, kind=kind, convert_timezone=False) echo(json.dumps(history, indent=2)) return None @cli.command() @click.option( "--count", required=False, default=False, is_flag=True, help="Count the number of videos on your Ring account", ) @click.option( "--download-all", required=False, default=False, is_flag=True, help="Download all videos on your Ring account", ) @click.option( "--download", required=False, default=False, is_flag=True, help="Download videos on your Ring account up to the max-count option", ) @click.option( "--max-count", required=False, default=300, help="Maximum count of videos to count or download from your Ring account", ) @click.option( "--download-to", required=False, default="./", help="Download location ending with a /", ) @click.option( "--device-name", "-dn", default=None, required=False, help="Name of the ring device, if omitted uses the first device returned", ) @pass_ring @click.pass_context async def videos( ctx, ring: Ring, count, download, download_all, max_count, download_to, device_name ): """Interact with ring videos.""" device = None if device_name and not (device := ring.get_video_device_by_name(device_name)): echo( f"No device with name {device_name} found. " + "List of found device names (kind) is:" ) return await ctx.invoke(list_command) if device and not device.has_capability("video"): echo(f"Device {device.name} is not a video device") return None # return the first device is implemented to be consistent with previous cli version if not device: if video_devices := ring.video_devices(): device = video_devices[0] else: echo( "No video devices found. " + "List of found device names (with device kind) is:" ) return await ctx.invoke(list_command) if not device: # Make mypy happy return None if ( not count and not download and not download_all and (last_recording_id := await device.async_get_last_recording_id()) and (url := await device.async_recording_url(last_recording_id)) ): echo("Last recording url is: " + url) return None events = None if download_all: download = True max_count = -1 async def _get_events(device, max_count): limit = 100 if max_count == -1 else min(100, max_count) events = [] history = await device.async_history(limit=limit) while len(history) > 0: events += history if (len(events) >= max_count and max_count != -1) or len(history) < limit: break history = await device.async_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 = await _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 = await _get_events(device, max_count) echo( f"\tDownloading {len(events)} videos linked on your Ring account.\n" "\tThis may take some time....\n" ) for counter, event in enumerate(events): filename = str(PurePath(download_to, _format_filename(device.name, event))) echo(f"\t{counter}/{len(events)} Downloading {filename}") await device.async_recording_download( event["id"], filename=filename, override=False ) return None return None @cli.group(invoke_without_command=True) @pass_ring @click.pass_context @click.option( "--device-name", "-dn", required=False, default=None, help="Name of the ring device", ) async def in_home_chime(ctx, ring: Ring, device_name): """View and manage the Doorbell in-home chime. To see the current in-home chime status of a device, only pass the device name.""" if "--help" in sys.argv: return device = _get_device( ring, ["authorized_doorbots", "doorbots"], RingDoorBell, device_name, device_description="doorbell", ) if ctx.invoked_subcommand is None: echo("Name: %s" % device.name) echo("ID: %s" % device.id) echo("Type: %s" % device.existing_doorbell_type) echo("Enabled: %s" % device.existing_doorbell_type_enabled) echo("Duration: %s" % device.existing_doorbell_type_duration) return None ctx.obj = device @in_home_chime.command(name="type") @pass_doorbell @click.pass_context @click.argument( "new_type", type=click.Choice(list(DOORBELL_EXISTING_TYPE.values()), case_sensitive=False), default=None, required=False, ) async def in_home_chime_type(ctx, device: RingDoorBell, new_type): """Get/set the type of In-home chime.""" if new_type is None: echo(device.existing_doorbell_type) return if device.family == "authorized_doorbots": exit( f"{device.name} is a shared device and you do not have permission to update this value" ) new_type_int = next(k for k, v in DOORBELL_EXISTING_TYPE.items() if v == new_type) await device.async_set_existing_doorbell_type(new_type_int) echo(f"{device.name}'s in-home chime type has been set to {new_type}") @in_home_chime.command() @pass_doorbell @click.pass_context @click.argument("enable", type=click.BOOL, default=None, required=False) async def enabled(ctx, device: RingDoorBell, enable: bool | None): """Gets/sets the in-home chime enabled status. ENABLE: 1/0, true/false, t/f, yes/no, y/n, and on/off """ if enable is None: echo(device.existing_doorbell_type_enabled) return if device.family == "authorized_doorbots": exit( f"{device.name} is a shared device and you do not have permission to update this value" ) await device.async_set_existing_doorbell_type_enabled(enable) echo( f"{device.name}'s in-home chime has been {'enabled' if enable else 'disabled'}" ) @in_home_chime.command() @pass_doorbell @click.pass_context @click.argument("duration", type=click.IntRange(0, 100), default=None, required=False) async def duration(ctx, device: RingDoorBell, duration: int | None): """Gets/sets the in-home chime duration. DURATION: Value between 0 and 100 """ if duration is None: echo(device.existing_doorbell_type_duration) return if device.family == "authorized_doorbots": exit( f"{device.name} is a shared device and you do not have permission to update this value" ) await device.async_set_existing_doorbell_type_duration(int(duration)) echo(f"{device.name}'s in-home chime duration has been set to {duration} seconds") async def ainput(string: str): loop = asyncio.get_event_loop() await loop.run_in_executor(None, lambda s=string: sys.stdout.write(s + " ")) # type: ignore[misc] return await loop.run_in_executor(None, sys.stdin.readline) def get_now_str(): return str(datetime.utcnow()) class _event_handler: # pylint:disable=invalid-name def __init__(self, ring: Ring) -> None: self.ring = ring def on_event(self, event: RingEvent) -> None: msg = ( get_now_str() + ": " + str(event) + " : Currently active count = " + str(len(self.ring.push_dings_data)) ) echo(msg) @cli.command @click.option( "--credentials-file", required=False, default=None, help=( "File to store push credentials, " + "if not provided credentials will be recreated from scratch" ), ) @click.option( "--store-credentials/--no-store-credentials", default=True, help="Whether or not to store the push credentials, default is false", ) @click.option( "--show-credentials", default=False, is_flag=True, help="Whether or not to show the push credentials, default is false", ) @pass_ring @click.pass_context async def listen( ctx, ring, store_credentials, credentials_file, show_credentials, ) -> None: """Listen to push notification like the ones sent to your phone.""" from ring_doorbell.listen import ( # pylint:disable=import-outside-toplevel RingEventListener, ) def credentials_updated_callback(credentials) -> None: if store_credentials: with open(credentials_file, "w", encoding="utf-8") as f: json.dump(credentials, f) else: echo("New push credentials created:") if show_credentials: echo(credentials) if not credentials_file: credentials_file = gcm_cache_file else: credentials_file = Path(credentials_file) credentials = None if store_credentials and credentials_file.is_file(): # already registered, load previous credentials with open(credentials_file, encoding="utf-8") as f: credentials = json.load(f) event_listener = RingEventListener(ring, credentials, credentials_updated_callback) await event_listener.start() event_listener.add_notification_callback(_event_handler(ring).on_event) await ainput("Listening, press enter to cancel\n") await event_listener.stop() @cli.command @click.option( "--device-name", required=False, default=None, help=("Name of the intercom if there are more than one."), ) @pass_ring @click.pass_context async def open_door(ctx, ring: Ring, device_name: str | None) -> None: """Open the door of a intercom device.""" device = _get_device( ring, ["intercoms", "other"], RingOther, device_name, device_description="intercom", ) await device.async_open_door() echo(f"{device.name} opened") if __name__ == "__main__": cli() # pylint: disable=no-value-for-parameter python-ring-doorbell-0.9.13/ring_doorbell/const.py000066400000000000000000000226121472135273200222140ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Constants and enums.""" from __future__ import annotations from enum import Enum, auto from typing import Final from ring_doorbell.exceptions import RingError class OAuth: """OAuth class constants.""" ENDPOINT = "https://oauth.ring.com/oauth/token" CLIENT_ID = "ring_official_android" SCOPE: Final[list[str]] = ["client"] class RingEventKind(Enum): """Enum of available ring events.""" DING = "ding" MOTION = "motion" INTERCOM_UNLOCK = "intercom_unlock" class RingCapability(Enum): """Enum of available ring events.""" VIDEO = auto() MOTION_DETECTION = auto() HISTORY = auto() LIGHT = auto() SIREN = auto() VOLUME = auto() BATTERY = auto() OPEN = auto() KNOCK = auto() PRE_ROLL = auto() DING = auto() @staticmethod def from_name(name: str) -> RingCapability: """Return ring capability from string value.""" capability = name.replace("-", "_").upper() for ring_capability in RingCapability: if ring_capability.name == capability: return ring_capability msg = f"Unknown ring capability {name}" raise RingError(msg) PACKAGE_NAME = "ring_doorbell" # timeout for HTTP requests TIMEOUT = 10 # longer default timeout for recording downloads - typical video file sizes # are ~12 MB and empirical testing reveals a ~20 second download time over a # fast connection, suggesting speed is largely governed by capacity of Ring # backend; to be safe, we factor in a worst case overhead and set it to 2 # minutes (this default can be overridden in method call) DEFAULT_VIDEO_DOWNLOAD_TIMEOUT = 120 # API endpoints API_VERSION = "11" API_URI = "https://api.ring.com" APP_API_URI = "https://prd-api-us.prd.rings.solutions" 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 FCM_RING_SENDER_ID = "876313859327" FCM_API_KEY = "AIzaSyCv-hdFBmmdBBJadNy-TFwB-xN_H5m3Bk8" FCM_PROJECT_ID = "ring-17770" FCM_APP_ID = "1:876313859327:android:e10ec6ddb3c81f39" CLI_TOKEN_FILE = "ring_token.cache" # noqa: S105 GCM_TOKEN_FILE = "ring_gcm_token.cache" # noqa: S105 CHIMES_ENDPOINT = "/clients_api/chimes/{0}" DEVICES_ENDPOINT = "/clients_api/ring_devices" DINGS_ENDPOINT = "/clients_api/dings/active" DOORBELLS_ENDPOINT = "/clients_api/doorbots/{0}" PERSIST_TOKEN_ENDPOINT = "/clients_api/device" # noqa: S105 SUBSCRIPTION_ENDPOINT = "/clients_api/device" GROUPS_ENDPOINT = "/groups/v1/locations/{0}/groups" LOCATIONS_HISTORY_ENDPOINT = "/evm/v2/history/locations/{0}" LOCATIONS_ENDPOINT = "/clients_api/locations/{0}" HEALTH_DOORBELL_ENDPOINT = DOORBELLS_ENDPOINT + "/health" HEALTH_CHIMES_ENDPOINT = CHIMES_ENDPOINT + "/health" LIGHTS_ENDPOINT = DOORBELLS_ENDPOINT + "/floodlight_light_{1}" LINKED_CHIMES_ENDPOINT = CHIMES_ENDPOINT + "/linked_doorbots" LIVE_STREAMING_ENDPOINT = DOORBELLS_ENDPOINT + "/live_view" NEW_SESSION_ENDPOINT = "/clients_api/session" RINGTONES_ENDPOINT = "/ringtones" SIREN_ENDPOINT = DOORBELLS_ENDPOINT + "/siren_{1}" SNAPSHOT_ENDPOINT = "/clients_api/snapshots/image/{0}" SNAPSHOT_TIMESTAMP_ENDPOINT = "/clients_api/snapshots/timestamps" TESTSOUND_CHIME_ENDPOINT = CHIMES_ENDPOINT + "/play_sound" URL_DOORBELL_HISTORY = DOORBELLS_ENDPOINT + "/history" URL_RECORDING = "/clients_api/dings/{0}/recording" URL_RECORDING_SHARE_PLAY = "/clients_api/dings/{0}/share/play" GROUP_DEVICES_ENDPOINT = GROUPS_ENDPOINT + "/{1}/devices" SETTINGS_ENDPOINT = "/devices/v1/devices/{0}/settings" # Alternative API for Intercom history, not used in favor of the DoorBell API URL_INTERCOM_HISTORY = LOCATIONS_HISTORY_ENDPOINT + "?ringtercom" INTERCOM_OPEN_ENDPOINT = "/commands/v1/devices/{0}/device_rpc" INTERCOM_INVITATIONS_ENDPOINT = LOCATIONS_ENDPOINT + "/invitations" INTERCOM_INVITATIONS_DELETE_ENDPOINT = LOCATIONS_ENDPOINT + "/invitations/{1}" INTERCOM_ALLOWED_USERS = LOCATIONS_ENDPOINT + "/users" # New API endpoints for web rtc streaming RTC_STREAMING_TICKET_ENDPOINT = "/api/v1/clap/ticket/request/signalsocket" RTC_STREAMING_WEB_SOCKET_ENDPOINT = "wss://api.prod.signalling.ring.devices.a2z.com:443/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-{0}&token={1}" KIND_DING = "ding" KIND_MOTION = "motion" KIND_INTERCOM_UNLOCK = "intercom_unlock" KIND_ALARM_MODE_NONE = "alarm_mode_none" KIND_ALARM_MODE_SOME = "alarm_mode_some" KIND_ALARM_SIREN = "alarm_siren" KIND_ALARM_SILENCED = "alarm_silenced" # chime test sound kinds CHIME_TEST_SOUND_KINDS = (KIND_DING, KIND_MOTION) # default values CHIME_VOL_MIN = 0 CHIME_VOL_MAX = 11 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 DOORBELL_EXISTING_DURATION_MIN = 0 DOORBELL_EXISTING_DURATION_MAX = 10 # 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"] DOORBELL_BATTERY_KINDS = ["df_doorbell_clownfish"] PEEPHOLE_CAM_KINDS = ["doorbell_portal"] DOORBELL_GEN2_KINDS = ["cocoa_doorbell", "cocoa_doorbell_v2"] FLOODLIGHT_CAM_KINDS = ["hp_cam_v1", "floodlight_v2"] FLOODLIGHT_CAM_PRO_KINDS = ["floodlight_pro"] FLOODLIGHT_CAM_PLUS_KINDS = ["cocoa_floodlight"] INDOOR_CAM_KINDS = ["stickup_cam_mini"] INDOOR_CAM_GEN2_KINDS = ["stickup_cam_mini_v2"] SPOTLIGHT_CAM_BATTERY_KINDS = ["stickup_cam_v4"] SPOTLIGHT_CAM_WIRED_KINDS = ["hp_cam_v2", "spotlightw_v2"] SPOTLIGHT_CAM_PLUS_KINDS = ["cocoa_spotlight"] SPOTLIGHT_CAM_PRO_KINDS = ["stickup_cam_longfin"] STICKUP_CAM_KINDS = ["stickup_cam", "stickup_cam_v3"] STICKUP_CAM_BATTERY_KINDS = ["stickup_cam_lunar"] STICKUP_CAM_ELITE_KINDS = ["stickup_cam_elite", "stickup_cam_wired"] STICKUP_CAM_WIRED_KINDS = STICKUP_CAM_ELITE_KINDS # Deprecated STICKUP_CAM_GEN3_KINDS = ["cocoa_camera"] BEAM_KINDS = ["beams_ct200_transformer"] INTERCOM_KINDS = ["intercom_handset_audio"] # error strings MSG_BOOLEAN_REQUIRED = "Boolean value is required." MSG_EXISTING_TYPE = f"Integer value where {DOORBELL_EXISTING_TYPE}." MSG_GENERIC_FAIL = "Sorry.. Something went wrong..." FILE_EXISTS = "The file {0} already exists." MSG_VOL_OUTBOUND = "Must be within the {0}-{1}." MSG_ALLOWED_VALUES = "Only the following values are allowed: {0}." MSG_EXPECTED_ATTRIBUTE_NOT_FOUND = "Couldn't find expected attribute: {0}." PUSH_ACTION_DING = "com.ring.push.HANDLE_NEW_DING" PUSH_ACTION_MOTION = "com.ring.push.HANDLE_NEW_motion" PUSH_ACTION_INTERCOM_UNLOCK = "com.ring.push.INTERCOM_UNLOCK_FROM_APP" PUSH_NOTIFICATION_DING = "com.ring.pn.live-event.ding" PUSH_NOTIFICATION_MOTION = "com.ring.pn.live-event.motion" PUSH_NOTIFICATION_INTERCOM = "com.ring.pn.live-event.intercom" PUSH_NOTIFICATION_INTERCOM_UNLOCK = "com.ring.pn.intercom.virtual.unlock" PUSH_NOTIFICATION_KINDS = { PUSH_ACTION_DING: KIND_DING, # legacy PUSH_NOTIFICATION_DING: KIND_DING, PUSH_ACTION_MOTION: KIND_MOTION, # legacy PUSH_NOTIFICATION_MOTION: KIND_MOTION, PUSH_NOTIFICATION_INTERCOM: KIND_DING, PUSH_ACTION_INTERCOM_UNLOCK: KIND_INTERCOM_UNLOCK, PUSH_NOTIFICATION_INTERCOM_UNLOCK: KIND_INTERCOM_UNLOCK, "com.ring.push.HANDLE_NEW_SECURITY_PANEL_MODE_NONE_NOTICE": KIND_ALARM_MODE_NONE, "com.ring.push.HANDLE_NEW_SECURITY_PANEL_MODE_SOME_NOTICE": KIND_ALARM_MODE_SOME, "com.ring.push.HANDLE_NEW_USER_SOUND_SIREN": KIND_ALARM_SIREN, "com.ring.push.HANDLE_NEW_NON_ALARM_SIREN_SILENCED": KIND_ALARM_SILENCED, } 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", } ICE_SERVERS = [ "stun:stun.kinesisvideo.us-east-1.amazonaws.com:443", "stun:stun.kinesisvideo.us-east-2.amazonaws.com:443", "stun:stun.kinesisvideo.us-west-2.amazonaws.com:443", "stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302", "stun:stun3.l.google.com:19302", "stun:stun4.l.google.com:19302", ] python-ring-doorbell-0.9.13/ring_doorbell/doorbot.py000066400000000000000000000461401472135273200225400ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Doorbell wrapper.""" from __future__ import annotations import asyncio import logging import time from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar import aiofiles 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_BATTERY_KINDS, DOORBELL_ELITE_KINDS, DOORBELL_EXISTING_DURATION_MAX, DOORBELL_EXISTING_DURATION_MIN, 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, ICE_SERVERS, LIVE_STREAMING_ENDPOINT, MSG_ALLOWED_VALUES, MSG_BOOLEAN_REQUIRED, MSG_EXISTING_TYPE, MSG_EXPECTED_ATTRIBUTE_NOT_FOUND, MSG_VOL_OUTBOUND, PEEPHOLE_CAM_KINDS, SETTINGS_ENDPOINT, SNAPSHOT_ENDPOINT, SNAPSHOT_TIMESTAMP_ENDPOINT, URL_RECORDING, URL_RECORDING_SHARE_PLAY, RingCapability, ) from ring_doorbell.exceptions import RingError from ring_doorbell.generic import RingGeneric from ring_doorbell.webrtcstream import RingWebRtcMessageCallback, RingWebRtcStream _LOGGER = logging.getLogger(__name__) class RingDoorBell(RingGeneric): """Implementation for Ring Doorbell.""" if TYPE_CHECKING: from ring_doorbell.ring import Ring def __init__(self, ring: Ring, device_api_id: int, *, shared: bool = False) -> None: """Initialise the doorbell.""" super().__init__(ring, device_api_id) self.shared = shared self._webrtc_streams: dict[str, RingWebRtcStream] = {} @property def family(self) -> str: """Return Ring device family type.""" return "authorized_doorbots" if self.shared else "doorbots" async def async_update_health_data(self) -> None: """Update health attrs.""" resp = await self._ring.async_query( HEALTH_DOORBELL_ENDPOINT.format(self.device_api_id) ) self._health_attrs = resp.json().get("device_health", {}) @property def model(self) -> str: # noqa: C901, PLR0911 """Return Ring device model name.""" if self.kind in DOORBELL_KINDS: return "Doorbell" if self.kind in DOORBELL_2_KINDS: return "Doorbell 2" if self.kind in DOORBELL_3_KINDS: return "Doorbell 3" if self.kind in DOORBELL_3_PLUS_KINDS: return "Doorbell 3 Plus" if self.kind in DOORBELL_4_KINDS: return "Doorbell 4" if self.kind in DOORBELL_PRO_KINDS: return "Doorbell Pro" if self.kind in DOORBELL_PRO_2_KINDS: return "Doorbell Pro 2" if self.kind in DOORBELL_ELITE_KINDS: return "Doorbell Elite" if self.kind in DOORBELL_WIRED_KINDS: return "Doorbell Wired" if self.kind in DOORBELL_BATTERY_KINDS: return "Battery Doorbell" if self.kind in DOORBELL_GEN2_KINDS: return "Doorbell (2nd Gen)" if self.kind in PEEPHOLE_CAM_KINDS: return "Peephole Cam" return "Unknown Doorbell" def has_capability(self, capability: RingCapability | str) -> bool: # noqa: PLR0911 """Return if device has specific capability.""" capability = ( capability if isinstance(capability, RingCapability) else RingCapability.from_name(capability) ) if capability == RingCapability.BATTERY: return self.kind in ( DOORBELL_KINDS + DOORBELL_2_KINDS + DOORBELL_3_KINDS + DOORBELL_3_PLUS_KINDS + DOORBELL_4_KINDS + DOORBELL_GEN2_KINDS + DOORBELL_BATTERY_KINDS + PEEPHOLE_CAM_KINDS ) if capability == RingCapability.KNOCK: return self.kind in PEEPHOLE_CAM_KINDS if capability == RingCapability.PRE_ROLL: return self.kind in DOORBELL_3_PLUS_KINDS if capability == RingCapability.VOLUME: return True if capability == RingCapability.HISTORY: return True if capability in [ RingCapability.MOTION_DETECTION, RingCapability.VIDEO, RingCapability.DING, ]: 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_BATTERY_KINDS + DOORBELL_GEN2_KINDS + DOORBELL_ELITE_KINDS + PEEPHOLE_CAM_KINDS ) return False @property def battery_life(self) -> int | None: """Return battery life.""" if ( bl1 := self._attrs.get("battery_life") ) is None and "battery_life_2" not in self._attrs: return None value = 0 if bl1: value += int(bl1) if bl2 := self._attrs.get("battery_life_2"): # Camera has two battery bays value += int(bl2) return min(value, 100) def _get_chime_setting(self, setting: str) -> Any | None: if (settings := self._attrs.get("settings")) and ( chime_settings := settings.get("chime_settings") ): return chime_settings.get(setting) return None @property def existing_doorbell_type(self) -> str | None: """ Return existing doorbell type. 0: Mechanical 1: Digital 2: Not Present """ try: if (dtype := self._get_chime_setting("type")) is not None: return DOORBELL_EXISTING_TYPE[dtype] except AttributeError: return None else: return None async def async_set_existing_doorbell_type(self, value: int) -> None: """ Return existing doorbell type. 0: Mechanical 1: Digital 2: Not Present """ if value not in DOORBELL_EXISTING_TYPE: msg = f"value must be in {MSG_EXISTING_TYPE}" raise RingError(msg) params = { "doorbot[description]": self.name, "doorbot[settings][chime_settings][type]": value, } if self.existing_doorbell_type: url = DOORBELLS_ENDPOINT.format(self.device_api_id) await self._ring.async_query(url, extra_params=params, method="PUT") @property def existing_doorbell_type_enabled(self) -> bool | None: """Return if existing doorbell type is enabled.""" if self.existing_doorbell_type: if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[2]: return None return self._get_chime_setting("enable") return False async def async_set_existing_doorbell_type_enabled(self, value: bool) -> None: # noqa: FBT001 """Enable/disable the existing doorbell if Digital/Mechanical.""" if self.existing_doorbell_type: if not isinstance(value, bool): raise RingError(MSG_BOOLEAN_REQUIRED) if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[2]: msg = "In-Home chime is not present." raise RingError(msg) int_value = int(value) params = { "doorbot[description]": self.name, "doorbot[settings][chime_settings][enable]": int_value, } url = DOORBELLS_ENDPOINT.format(self.device_api_id) await self._ring.async_query(url, extra_params=params, method="PUT") @property def existing_doorbell_type_duration(self) -> int | None: """Return duration for Digital chime.""" if ( self.existing_doorbell_type and self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[1] ): return self._get_chime_setting("duration") return None async def async_set_existing_doorbell_type_duration(self, value: int) -> None: """Set duration for Digital chime.""" if self.existing_doorbell_type: if not ( (isinstance(value, int)) and ( DOORBELL_EXISTING_DURATION_MIN <= value <= DOORBELL_EXISTING_DURATION_MAX ) ): raise RingError( MSG_VOL_OUTBOUND.format( DOORBELL_EXISTING_DURATION_MIN, DOORBELL_EXISTING_DURATION_MAX ) ) if self.existing_doorbell_type == DOORBELL_EXISTING_TYPE[1]: params = { "doorbot[description]": self.name, "doorbot[settings][chime_settings][duration]": value, } url = DOORBELLS_ENDPOINT.format(self.device_api_id) await self._ring.async_query(url, extra_params=params, method="PUT") async def async_get_last_recording_id(self) -> int | None: """Return the last recording ID.""" try: res = await self.async_history(limit=1) return res[0].get("id") if res else None except (IndexError, TypeError): return None async def async_get_live_streaming_json(self) -> dict[str, Any] | None: """Return JSON for live streaming.""" url = LIVE_STREAMING_ENDPOINT.format(self.device_api_id) req = await self._ring.async_query( url, method="POST", base_uri="https://app.ring.com" ) if req and req.status_code == 200: url = DINGS_ENDPOINT try: resp = await self._ring.async_query(url) return resp.json()[0] except (IndexError, TypeError): pass return None async def async_recording_download( self, recording_id: int, filename: str | None = None, *, override: bool = False, timeout: int = DEFAULT_VIDEO_DOWNLOAD_TIMEOUT, ) -> bytes | None: """Save a recording in MP4 format to a file or return raw.""" if not self.has_subscription: msg = "Your Ring account does not have an active subscription." _LOGGER.warning(msg) return None url = URL_RECORDING.format(recording_id) try: # Video download needs a longer timeout to get the large video file req = await self._ring.async_query(url, timeout=timeout) if req.status_code == 200: if filename: if Path(filename).is_file() and not override: raise RingError(FILE_EXISTS.format(filename)) async with aiofiles.open(filename, "wb") as recording: await recording.write(req.content) return None else: return req.content else: msg = ( f"Could not get recording at url {url}, " f"status code is {req.status_code}" ) raise RingError(msg) except OSError as error: msg = f"Error downloading recording {recording_id}: {error}" _LOGGER.exception(msg) raise RingError(msg) from error async def async_recording_url(self, recording_id: int) -> str | None: """Return HTTPS recording URL.""" if not self.has_subscription: msg = "Your Ring account does not have an active subscription." _LOGGER.warning(msg) return None url = URL_RECORDING_SHARE_PLAY.format(recording_id) req = await self._ring.async_query(url) data = req.json() if req and req.status_code == 200 and data is not None: return data["url"] return None @property def subscribed(self) -> bool: """Return if is online.""" result = self._attrs.get("subscribed") return result is not None @property def subscribed_motion(self) -> bool: """Return if is subscribed_motion.""" result = self._attrs.get("subscribed_motions") return result is not None @property def has_subscription(self) -> bool: """Return boolean if the account has subscription.""" if features := self._attrs.get("features"): return features.get("show_recordings", False) return False @property def volume(self) -> int: """Return the volume.""" return self._attrs["settings"].get("doorbell_volume", 0) async def async_set_volume(self, value: int) -> None: """Set the volume.""" if not ( (isinstance(value, int)) and (DOORBELL_VOL_MIN <= value <= DOORBELL_VOL_MAX) ): raise RingError(MSG_VOL_OUTBOUND.format(DOORBELL_VOL_MIN, DOORBELL_VOL_MAX)) params = { "doorbot[description]": self.name, "doorbot[settings][doorbell_volume]": str(value), } url = DOORBELLS_ENDPOINT.format(self.device_api_id) await self._ring.async_query(url, extra_params=params, method="PUT") @property def connection_status(self) -> str | None: """Return connection status.""" if alerts := self._attrs.get("alerts"): return alerts.get("connection") return None async def async_get_snapshot( self, retries: int = 3, delay: int = 1, filename: str | None = None ) -> bytes | None: """Take a snapshot and download it.""" url = SNAPSHOT_TIMESTAMP_ENDPOINT payload = {"doorbot_ids": [self._attrs.get("id")]} await self._ring.async_query(url, method="POST", json=payload) request_time = time.time() for _ in range(retries): await asyncio.sleep(delay) resp = await self._ring.async_query(url, method="POST", json=payload) response = resp.json() if response["timestamps"][0]["timestamp"] / 1000 > request_time: resp = await self._ring.async_query( SNAPSHOT_ENDPOINT.format(self._attrs.get("id")) ) snapshot = resp.content if filename: async with aiofiles.open(filename, "wb") as jpg: await jpg.write(snapshot) return None return snapshot return None def _motion_detection_state(self) -> bool | None: if settings := self._attrs.get("settings"): return settings.get("motion_detection_enabled") return None @property def motion_detection(self) -> bool: """Return motion detection enabled state.""" return state if (state := self._motion_detection_state()) else False async def async_set_motion_detection(self, state: bool) -> None: # noqa: FBT001 """Set the motion detection enabled state.""" values = [True, False] if state not in values: raise RingError(MSG_ALLOWED_VALUES.format("True, False")) if self._motion_detection_state() is None: _LOGGER.warning( "%s", MSG_EXPECTED_ATTRIBUTE_NOT_FOUND.format( "settings[motion_detection_enabled]" ), ) return url = SETTINGS_ENDPOINT.format(self.device_api_id) payload = {"motion_settings": {"motion_detection_enabled": state}} await self._ring.async_query(url, method="PATCH", json=payload) async def generate_webrtc_stream( self, sdp_offer: str, *, keep_alive_timeout: int | None = 30 ) -> str: """Generate the rtc stream.""" if session_id := RingWebRtcStream.get_sdp_session_id(sdp_offer): async def _close_callback() -> None: await self.close_webrtc_stream(session_id) stream = RingWebRtcStream( self._ring, self.device_api_id, keep_alive_timeout=keep_alive_timeout, on_close_callback=_close_callback, ) sdp_answer = await stream.generate(sdp_offer) # generate will raise if no sdp answer as no callback passed assert sdp_answer # noqa: S101 self._webrtc_streams[session_id] = stream return sdp_answer msg = "Unable to generate the stream, could not extract session id from offer." raise RingError(msg) async def generate_async_webrtc_stream( self, sdp_offer: str, session_id: str, on_message_callback: RingWebRtcMessageCallback, *, keep_alive_timeout: int | None = 60 * 5, ) -> None: """Generate the rtc stream. Will callback with answers and ICE candidates.""" async def _close_callback() -> None: await self.close_webrtc_stream(session_id) stream = RingWebRtcStream( self._ring, self.device_api_id, on_message_callback=on_message_callback, keep_alive_timeout=keep_alive_timeout, on_close_callback=_close_callback, ) self._webrtc_streams[session_id] = stream await stream.generate(sdp_offer) async def on_webrtc_candidate( self, session_id: str, candidate: str, multi_line_index: int ) -> None: """Send an ICE candidate.""" if stream := self._webrtc_streams.get(session_id): await stream.on_ice_candidate(candidate, multi_line_index) else: msg = "Ice candidate received before stream has been created." raise RingError(msg) async def close_webrtc_stream(self, session_id: str) -> None: """Close the rtc stream.""" stream = self._webrtc_streams.pop(session_id, None) if stream: await stream.close() def sync_close_webrtc_stream(self, session_id: str) -> None: """Close the rtc stream.""" stream = self._webrtc_streams.pop(session_id, None) if stream: stream.sync_close() async def keep_alive_webrtc_stream(self, sdp_session_id: str) -> None: """Keep alive the rtc stream.""" stream = self._webrtc_streams.get(sdp_session_id, None) if stream: await stream.keep_alive() def get_ice_servers(self) -> list[str]: """Return the ICE servers.""" return ICE_SERVERS DEPRECATED_API_QUERIES: ClassVar = { *RingGeneric.DEPRECATED_API_QUERIES, "update_health_data", "recording_download", "recording_url", "get_snapshot", } DEPRECATED_API_PROPERTY_GETTERS: ClassVar = { *RingGeneric.DEPRECATED_API_PROPERTY_GETTERS, "last_recording_id", "live_streaming_json", } DEPRECATED_API_PROPERTY_SETTERS: ClassVar = { *RingGeneric.DEPRECATED_API_PROPERTY_SETTERS, "existing_doorbell_type", "existing_doorbell_type_enabled", "existing_doorbell_type_duration", "volume", "motion_detection", } python-ring-doorbell-0.9.13/ring_doorbell/event.py000066400000000000000000000020131472135273200222000ustar00rootroot00000000000000"""Module for ring events.""" from __future__ import annotations from dataclasses import dataclass from typing import Any, NamedTuple @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 is_update: bool = False def __getitem__(self, key: str) -> Any: """Get a value by string.""" return getattr(self, key) def get(self, key: str) -> Any | None: """Get a value by string and return None if not present.""" return getattr(self, key) if hasattr(self, key) else None def get_key(self) -> RingEventKey: """Return the identificationkey for the event.""" return RingEventKey(self.id, self.doorbot_id, self.kind, self.now) class RingEventKey(NamedTuple): """Class to identify an event. Used for determining if messages are updates to events. """ id: int doorbot_id: int kind: str now: float python-ring-doorbell-0.9.13/ring_doorbell/exceptions.py000066400000000000000000000005671472135273200232540ustar00rootroot00000000000000"""ring-doorbell exceptions.""" class RingError(Exception): """Base exception for device errors.""" class Requires2FAError(RingError): """Exception that 2FA is required.""" class AuthenticationError(RingError): """Exception for ring authentication errors.""" class RingTimeout(RingError): # noqa: N818 """Exception for ring authentication errors.""" python-ring-doorbell-0.9.13/ring_doorbell/generic.py000066400000000000000000000202331472135273200224770ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring RingGeneric wrapper.""" # pylint: disable=useless-object-inheritance from __future__ import annotations import logging from typing import TYPE_CHECKING, Any, ClassVar import pytz from ring_doorbell.const import URL_DOORBELL_HISTORY, RingCapability from ring_doorbell.util import ( parse_datetime, ) _LOGGER = logging.getLogger(__name__) class RingGeneric: """Generic Implementation for Ring Chime/Doorbell.""" if TYPE_CHECKING: from ring_doorbell.ring import Ring def __init__(self, ring: Ring, device_api_id: int) -> None: """Initialize Ring Generic.""" self._ring = ring # This is the account ID of the device. # Not the same as device ID. self.device_api_id = device_api_id self.capability = False self.alert = None self._health_attrs: dict[str, Any] = {} self._last_history: list[dict[str, Any]] = [] # alerts notifications self.alert_expires_at = None def __repr__(self) -> str: """Return __repr__.""" return f"<{self.__class__.__name__}: {self.name}>" def __str__(self) -> str: """Return string representation of device.""" return f"{self.name} ({self.kind})" async def async_update(self) -> None: """Update this device info.""" await self.async_update_health_data() async def async_update_health_data(self) -> None: """Update the health data.""" raise NotImplementedError @property def _attrs(self) -> dict[str, Any]: """Return attributes.""" return self._ring.devices_data[self.family][self.device_api_id] @property def id(self) -> int: """Return ID.""" return self.device_api_id @property def name(self) -> str: """Return name.""" return self._attrs["description"] @property def device_id(self) -> str: """Return device ID. This is the device_id returned by the api, usually the MAC. Not to be confused with the id for the device """ return self._attrs["device_id"] @property def location_id(self) -> str | None: """Return location id.""" return self._attrs.get("location_id", None) @property def family(self) -> str: """Return Ring device family type.""" raise NotImplementedError @property def model(self) -> str: """Return Ring device model name.""" raise NotImplementedError @property def battery_life(self) -> int | None: """Return battery life.""" raise NotImplementedError def has_capability(self, capability: RingCapability | str) -> bool: # noqa: ARG002 """Return if device has specific capability.""" return self.capability @property def address(self) -> str | None: """Return address.""" return self._attrs.get("address") @property def firmware(self) -> str | None: """Return firmware.""" return self._attrs.get("firmware_version") @property def latitude(self) -> float | None: """Return latitude attr.""" return self._attrs.get("latitude") @property def longitude(self) -> float | None: """Return longitude attr.""" return self._attrs.get("longitude") @property def kind(self) -> str: """Return kind attr.""" return self._attrs["kind"] @property def timezone(self) -> str | None: """Return timezone.""" return self._attrs.get("time_zone") @property def wifi_name(self) -> str | None: """Return wifi ESSID name. Requires health data to be updated. """ return self._health_attrs.get("wifi_name") @property def wifi_signal_strength(self) -> int | None: """Return wifi RSSI. Requires health data to be updated. """ return self._health_attrs.get("latest_signal_strength") @property def wifi_signal_category(self) -> str | None: """Return wifi signal category. Requires health data to be updated. """ return self._health_attrs.get("latest_signal_category") @property def last_history(self) -> list[dict[str, Any]]: """Return the result of the last history query.""" return self._last_history async def async_history( # noqa: C901, PLR0913, PLR0912 self, *, limit: int = 30, timezone: str | None = None, kind: str | None = None, enforce_limit: bool = False, older_than: int | None = None, retry: int = 8, convert_timezone: bool = True, ) -> list[dict[str, Any]]: """ Return history with datetime objects. :param limit: specify number of objects to be returned :param timezone: determine which timezone to convert data objects :param kind: filter by kind (ding, motion, on_demand) :param enforce_limit: when True, this will enforce the limit and kind :param older_than: return older objects than the passed event_id :param retry: determine the max number of attempts to archive the limit """ if not self.has_capability("history"): return [] queries = 0 original_limit = limit # set cap for max queries # pylint:disable=consider-using-min-builtin retry = min(retry, 10) while True: params = {"limit": limit} if older_than: params["older_than"] = older_than url = URL_DOORBELL_HISTORY.format(self.device_api_id) resp = await self._ring.async_query(url, extra_params=params) response = resp.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 if timezone: mytz = pytz.timezone(timezone) for entry in response: utc_dt = parse_datetime(entry["created_at"]) if timezone: tz_dt = utc_dt.astimezone(mytz) entry["created_at"] = tz_dt else: entry["created_at"] = utc_dt if enforce_limit: # return because already matched the number # of events by kind if len(response) >= original_limit: return response[:original_limit] # ensure the loop will exit after max queries queries += 1 if queries == retry: _LOGGER.debug( "Could not find total of %s of kind %s", original_limit, kind ) break # ensure the kind objects returned to match limit limit = limit * 2 else: break self._last_history = response return self._last_history DEPRECATED_API_QUERIES: ClassVar = { "history", "update", "update_health_data", } DEPRECATED_API_PROPERTY_GETTERS: ClassVar[set[str]] = set() DEPRECATED_API_PROPERTY_SETTERS: ClassVar[set[str]] = set() if not TYPE_CHECKING: def __getattr__(self, name: str) -> Any: """Get a deprecated attribute or raise an error.""" if name in self.DEPRECATED_API_QUERIES: return self._ring.auth._dep_handler.get_api_query(self, name) # noqa: SLF001 if name in self.DEPRECATED_API_PROPERTY_GETTERS: return self._ring.auth._dep_handler.get_api_property(self, name) # noqa: SLF001 msg = f"{self.__class__.__name__} has no attribute {name!r}" raise AttributeError(msg) def __setattr__(self, name: str, value: Any) -> None: """Set a deprecated attribute or raise an error.""" if name in self.DEPRECATED_API_PROPERTY_SETTERS: self._ring.auth._dep_handler.set_api_property(self, name, value) # noqa: SLF001 else: super().__setattr__(name, value) python-ring-doorbell-0.9.13/ring_doorbell/group.py000066400000000000000000000106561472135273200222270ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring light group wrapper.""" from __future__ import annotations import logging import warnings from typing import TYPE_CHECKING, Any, ClassVar from ring_doorbell.const import ( GROUP_DEVICES_ENDPOINT, MSG_ALLOWED_VALUES, RingCapability, ) from ring_doorbell.exceptions import RingError _LOGGER = logging.getLogger(__name__) class RingLightGroup: """Implementation for RingLightGroup.""" if TYPE_CHECKING: from ring_doorbell.ring import Ring def __init__(self, ring: Ring, group_id: str) -> None: """Initialize Ring Light Group.""" self._ring = ring self.group_id = group_id # pylint:disable=invalid-name self._health_attrs: dict[str, Any] = {} self._health_attrs_fetched = False def __repr__(self) -> str: """Return __repr__.""" return f"<{self.__class__.__name__}: {self.name}>" async def async_update(self) -> None: """Update this device info.""" url = GROUP_DEVICES_ENDPOINT.format(self.location_id, self.group_id) resp = await self._ring.async_query(url) self._health_attrs = resp.json() self._health_attrs_fetched = True @property def _attrs(self) -> dict[str, Any]: """Return attributes.""" return self._ring.groups_data[self.group_id] @property def id(self) -> str: """Return ID.""" return self.group_id @property def name(self) -> str: """Return name.""" return self._attrs["name"] @property def family(self) -> str: """Return Ring device family type.""" return "group" @property def device_id(self) -> str: """Return group ID. Deprecated.""" warnings.warn( "RingLightGroup.device_id is deprecated; use group_id", DeprecationWarning, stacklevel=1, ) return self.group_id @property def location_id(self) -> str: """Return group location ID.""" return self._attrs["location_id"] @property def model(self) -> str: """Return Ring device model name.""" return "Light Group" def has_capability(self, capability: RingCapability | str) -> bool: """Return if device has specific capability.""" capability = ( capability if isinstance(capability, RingCapability) else RingCapability.from_name(capability) ) return capability == RingCapability.LIGHT @property def lights(self) -> bool: """Return lights status.""" if not self._health_attrs_fetched: msg = ( "You need to call update on the " "group before accessing the lights property." ) raise RingError(msg) return self._health_attrs["lights_on"] async def async_set_lights( self, state: bool | tuple[bool, int], duration: int | None = None, ) -> None: """Control the lights.""" values = ["True", "False"] if isinstance(state, tuple): state, duration = state if not isinstance(state, bool): raise RingError(MSG_ALLOWED_VALUES.format(", ".join(values))) url = GROUP_DEVICES_ENDPOINT.format(self.location_id, self.group_id) payload: dict[str, dict[str, bool | int]] = {"lights_on": {"enabled": state}} if duration is not None: payload["lights_on"]["duration_seconds"] = duration await self._ring.async_query(url, method="POST", json=payload) await self.async_update() DEPRECATED_API_QUERIES: ClassVar = { "update", } DEPRECATED_API_PROPERTY_SETTERS: ClassVar = { "lights", } if not TYPE_CHECKING: def __getattr__(self, name: str) -> Any: """Get a deprecated attribute or raise an error.""" if name in self.DEPRECATED_API_QUERIES: return self._ring.auth._dep_handler.get_api_query(self, name) # noqa: SLF001 msg = f"{self.__class__.__name__} has no attribute {name!r}" raise AttributeError(msg) def __setattr__(self, name: str, value: Any) -> None: """Set a deprecated attribute or raise an error.""" if name in self.DEPRECATED_API_PROPERTY_SETTERS: self._ring.auth._dep_handler.set_api_property(self, name, value) # noqa: SLF001 else: super().__setattr__(name, value) python-ring-doorbell-0.9.13/ring_doorbell/listen/000077500000000000000000000000001472135273200220075ustar00rootroot00000000000000python-ring-doorbell-0.9.13/ring_doorbell/listen/__init__.py000066400000000000000000000005151472135273200241210ustar00rootroot00000000000000"""Package for listener modules.""" from .eventlistener import RingEventListener from .listenerconfig import RingEventListenerConfig # can_listen used to be checkable to see if the optional listen extra installed. # Now installed as default. can_listen = True __all__ = [ "RingEventListener", "RingEventListenerConfig", ] python-ring-doorbell-0.9.13/ring_doorbell/listen/eventlistener.py000066400000000000000000000275031472135273200252570ustar00rootroot00000000000000"""Module for listening to firebase cloud messages and updating dings.""" from __future__ import annotations import asyncio import json import logging import time from typing import TYPE_CHECKING, Any, Callable from async_timeout import timeout as asyncio_timeout from firebase_messaging import FcmPushClient, FcmRegisterConfig from ring_doorbell.const import ( API_URI, API_VERSION, DEFAULT_LISTEN_EVENT_EXPIRES_IN, FCM_API_KEY, FCM_APP_ID, FCM_PROJECT_ID, FCM_RING_SENDER_ID, KIND_DING, KIND_INTERCOM_UNLOCK, KIND_MOTION, PUSH_ACTION_DING, PUSH_ACTION_INTERCOM_UNLOCK, PUSH_ACTION_MOTION, PUSH_NOTIFICATION_KINDS, SUBSCRIPTION_ENDPOINT, ) from ring_doorbell.event import RingEvent, RingEventKey from ring_doorbell.exceptions import RingError from ring_doorbell.util import parse_datetime from .listenerconfig import RingEventListenerConfig if TYPE_CHECKING: from ring_doorbell.ring import Ring _logger = logging.getLogger(__name__) OnNotificationCallable = Callable[[RingEvent], None] CredentialsUpdatedCallable = Callable[[dict[str, Any]], None] class RingEventListener: """Class to connect to firebase cloud messaging.""" SESSION_REFRESH_INTERVAL = 60 * 60 * 12 def __init__( self, ring: Ring, credentials: dict[str, Any] | None = None, credentials_updated_callback: CredentialsUpdatedCallable | None = None, *, config: RingEventListenerConfig | None = None, ) -> None: """Initialise the event listener with credentials. Provide a callback for when credentials are updated by FCM. """ self._ring = ring self._callbacks: dict[int, OnNotificationCallable] = {} self.subscribed = False self.started = False self._device_model = self._ring.auth.get_device_model() self._credentials = credentials self._credentials_updated_callback = credentials_updated_callback self._receiver: FcmPushClient | None = None self._config: RingEventListenerConfig = ( config or RingEventListenerConfig.default_config() ) self._subscription_counter = 1 self._intercom_unlock_counter: dict[int, int] = {} self.session_refresh_task: asyncio.Task | None = None self.fcm_token: str | None = None self._seen_events: set[RingEventKey] = set() def _credentials_updated_cb(self, creds: dict[str, Any]) -> None: self._credentials = creds if self._credentials_updated_callback: self._credentials_updated_callback(creds) async def add_subscription_to_ring(self, token: str) -> None: """Add subscription to ring.""" if not self._ring.session: await self._ring.async_create_session() session_patch_data = { "device": { "metadata": { "api_version": API_VERSION, "device_model": self._device_model, "pn_dict_version": "2.0.0", "pn_service": "fcm", }, "os": "android", "push_notification_token": token, } } resp = await self._ring.auth.async_query( API_URI + SUBSCRIPTION_ENDPOINT, method="PATCH", json=session_patch_data, raise_for_status=False, ) if resp.status_code != 204: _logger.error( "Unable to checkin to listen service, " "response was %s %s, event listener not started", resp.status_code, resp.text, ) self.subscribed = False return self.subscribed = True # Update devices for the intercom unlock events if not self._ring.devices_data: await self._ring.async_update_devices() def add_notification_callback(self, callback: OnNotificationCallable) -> int: """Add a callback to be notified on event.""" sub_id = self._subscription_counter self._callbacks[sub_id] = callback self._subscription_counter += 1 return sub_id def remove_notification_callback(self, subscription_id: int) -> None: """Remove a notification callback by id.""" if subscription_id == 1: msg = "Cannot remove the default callback for ring-doorbell with value 1" raise RingError(msg) if subscription_id not in self._callbacks: msg = f"ID {subscription_id} is not a valid callback id" raise RingError(msg) del self._callbacks[subscription_id] async def stop(self) -> None: """Stop the listener.""" self.started = False if self._receiver: await self._receiver.stop() refresh_task = self.session_refresh_task self.session_refresh_task = None if refresh_task and not refresh_task.done(): refresh_task.cancel() self._callbacks = {} async def start( self, *, timeout: int = 10, ) -> bool: """Start the listener.""" _logger.debug("Starting event listener") if not self._receiver: fcm_config = FcmRegisterConfig( FCM_PROJECT_ID, FCM_APP_ID, FCM_API_KEY, FCM_RING_SENDER_ID ) self._receiver = FcmPushClient( self._on_notification, fcm_config, self._credentials, self._credentials_updated_cb, config=self._config, http_client_session=self._ring.auth._session, # noqa: SLF001 ) self.fcm_token = await self._receiver.checkin_or_register() if not self.fcm_token: _logger.error( "Ring listener unable to check in to fcm, " "event listener not started" ) return False if not self.subscribed: await self.add_subscription_to_ring(self.fcm_token) if self.subscribed: self.add_notification_callback(self._ring._add_event_to_dings_data) # noqa: SLF001 async with asyncio_timeout(timeout): await self._receiver.start() self.started = True self.session_refresh_task = asyncio.create_task( self._periodic_session_refresh() ) _logger.debug("Started event listener") return self.started async def _periodic_session_refresh(self) -> None: while self.started: now = time.monotonic() if TYPE_CHECKING: assert self._ring.session_refresh_time assert self.fcm_token since_refresh = now - self._ring.session_refresh_time if since_refresh > self.SESSION_REFRESH_INTERVAL: _logger.debug("Refreshing ring session") await self._ring.async_create_session() await self.add_subscription_to_ring(self.fcm_token) break sleep_for = 1 + time.monotonic() - self._ring.session_refresh_time await asyncio.sleep(sleep_for) def _get_ding_event(self, gcm_data: dict[str, Any]) -> RingEvent: ding = gcm_data["ding"] action = gcm_data["action"] subtype = gcm_data["subtype"] if action.lower() == PUSH_ACTION_MOTION.lower(): kind = KIND_MOTION state = subtype elif action.lower == PUSH_ACTION_DING.lower(): kind = KIND_DING state = "ringing" else: kind = action state = subtype created_at = ding["created_at"] create_seconds = parse_datetime(created_at).timestamp() return RingEvent( id=ding["id"], kind=kind, doorbot_id=ding["doorbot_id"], device_name=ding["device_name"], device_kind=ding["device_kind"], now=create_seconds, expires_in=DEFAULT_LISTEN_EVENT_EXPIRES_IN, state=state, ) def _get_intercom_unlock_event(self, gcm_data: dict[str, Any]) -> RingEvent | None: device_api_id = gcm_data["alarm_meta"]["device_zid"] if (device := self._ring.get_device_by_api_id(device_api_id)) is None: _logger.debug("Event received for unknown device id: %s", device_api_id) return None if device_api_id not in self._intercom_unlock_counter: self._intercom_unlock_counter[device_api_id] = 0 self._intercom_unlock_counter[device_api_id] += 1 return RingEvent( id=self._intercom_unlock_counter[device_api_id], kind=KIND_INTERCOM_UNLOCK, doorbot_id=device_api_id, device_name=device.name, device_kind=device.kind, now=time.time(), expires_in=DEFAULT_LISTEN_EVENT_EXPIRES_IN, state="unlock", ) def _check_is_update(self, ring_event: RingEvent) -> None: """Battery doorbells send two events. First without an image and the second with an image. """ now = time.time() seen_events = { key for key in self._seen_events if (now - key.now) < DEFAULT_LISTEN_EVENT_EXPIRES_IN } event_key = ring_event.get_key() if event_key in seen_events: ring_event.is_update = True else: seen_events.add(event_key) self._seen_events = seen_events def _on_notification( self, notification: dict[str, dict[str, str]], persistent_id: str, # noqa: ARG002 obj: Any | None = None, # noqa: ARG002 ) -> None: msg_data = notification["data"] if "gcmData" in msg_data: gcm_data = json.loads(notification["data"]["gcmData"]) ring_event = self._get_legacy_ring_event(gcm_data) else: ring_event = self._get_ring_event(msg_data) if ring_event: self._check_is_update(ring_event) _logger.debug("Event received %s", ring_event) for callback in self._callbacks.values(): callback(ring_event) else: _logger.debug("Unknown event received %s", msg_data) def _get_ring_event(self, msg_data: dict) -> RingEvent | None: if (android_config_str := msg_data.get("android_config")) is None or ( data_str := msg_data.get("data") ) is None: _logger.debug( "Unexpected alert type in fcm message data. Full message is:\n%s", json.dumps(msg_data), ) return None android_config = json.loads(android_config_str) data = json.loads(data_str) event_category = android_config["category"] event_kind = PUSH_NOTIFICATION_KINDS.get(event_category, "Unknown") device = data["device"] event = data["event"] event_id = int(event["ding"]["id"]) created_at = event["ding"]["created_at"] create_seconds = parse_datetime(created_at).timestamp() return RingEvent( event_id, device["id"], device_name=device.get("name"), device_kind=device.get("kind"), kind=event_kind, now=create_seconds, expires_in=DEFAULT_LISTEN_EVENT_EXPIRES_IN, state=event["ding"]["subtype"], ) def _get_legacy_ring_event(self, gcm_data: dict) -> RingEvent | None: re: RingEvent | None = None if "ding" in gcm_data: re = self._get_ding_event(gcm_data) elif gcm_data.get("action") == PUSH_ACTION_INTERCOM_UNLOCK: re = self._get_intercom_unlock_event(gcm_data) elif "community_alert" not in gcm_data: _logger.debug( "Unexpected alert type in gcmData. Full message is:\n%s", json.dumps(gcm_data), ) return None return re python-ring-doorbell-0.9.13/ring_doorbell/listen/listenerconfig.py000066400000000000000000000010521472135273200253720ustar00rootroot00000000000000"""Module for RingEventListenerConfig.""" from __future__ import annotations from firebase_messaging import FcmPushClientConfig class RingEventListenerConfig(FcmPushClientConfig): """Configuration class for event listener.""" @staticmethod def default_config() -> RingEventListenerConfig: """Get an instance of the default config.""" config = RingEventListenerConfig() config.server_heartbeat_interval = 60 config.client_heartbeat_interval = 120 config.monitor_interval = 15 return config python-ring-doorbell-0.9.13/ring_doorbell/other.py000066400000000000000000000234011472135273200222040ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Other (Intercom) wrapper.""" from __future__ import annotations import json import logging import uuid from typing import TYPE_CHECKING, Any, ClassVar from ring_doorbell.const import ( DOORBELLS_ENDPOINT, HEALTH_DOORBELL_ENDPOINT, INTERCOM_ALLOWED_USERS, INTERCOM_INVITATIONS_DELETE_ENDPOINT, INTERCOM_INVITATIONS_ENDPOINT, INTERCOM_KINDS, INTERCOM_OPEN_ENDPOINT, MIC_VOL_MAX, MIC_VOL_MIN, MSG_VOL_OUTBOUND, OTHER_DOORBELL_VOL_MAX, OTHER_DOORBELL_VOL_MIN, SETTINGS_ENDPOINT, VOICE_VOL_MAX, VOICE_VOL_MIN, RingCapability, ) from ring_doorbell.exceptions import RingError from ring_doorbell.generic import RingGeneric _LOGGER = logging.getLogger(__name__) class RingOther(RingGeneric): """Implementation for Ring Intercom.""" if TYPE_CHECKING: from ring_doorbell.ring import Ring def __init__(self, ring: Ring, device_api_id: int, *, shared: bool = False) -> None: """Initialise the other devices.""" super().__init__(ring, device_api_id) self.shared = shared @property def family(self) -> str: """Return Ring device family type.""" return "other" async def async_update_health_data(self) -> None: """Update health attrs.""" resp = await self._ring.async_query( HEALTH_DOORBELL_ENDPOINT.format(self.device_api_id) ) self._health_attrs = resp.json().get("device_health", {}) @property def model(self) -> str: """Return Ring device model name.""" if self.kind in INTERCOM_KINDS: return "Intercom" return "Unknown Other" def has_capability(self, capability: RingCapability | str) -> bool: """Return if device has specific capability.""" capability = ( capability if isinstance(capability, RingCapability) else RingCapability.from_name(capability) ) if capability in [ RingCapability.OPEN, RingCapability.HISTORY, RingCapability.DING, ]: return self.kind in INTERCOM_KINDS return False @property def battery_life(self) -> int | None: """Return battery life.""" if self.kind in INTERCOM_KINDS: if self._attrs.get("battery_life") is None: return None value = int(self._attrs.get("battery_life", 0)) if value and value > 100: value = 100 return value return None @property def subscribed(self) -> bool: """Return if is online.""" if self.kind in INTERCOM_KINDS: result = self._attrs.get("subscribed") return result is not None return False @property def has_subscription(self) -> bool: """Return boolean if the account has subscription.""" if self.kind in INTERCOM_KINDS and (features := self._attrs.get("features")): return features.get("show_recordings", False) return False @property def unlock_duration(self) -> str | None: """Return time unlock switch is held closed.""" return ( json.loads(self._attrs["settings"]["intercom_settings"]["config"]) .get("analog", {}) .get("unlock_duration") ) @property def doorbell_volume(self) -> int: """Return doorbell volume.""" if self.kind in INTERCOM_KINDS: return self._attrs["settings"].get("doorbell_volume", 0) return 0 async def async_set_doorbell_volume(self, value: int) -> None: """Set the doorbell volume.""" if not ( (isinstance(value, int)) and (OTHER_DOORBELL_VOL_MIN <= value <= OTHER_DOORBELL_VOL_MAX) ): raise RingError( MSG_VOL_OUTBOUND.format(OTHER_DOORBELL_VOL_MIN, OTHER_DOORBELL_VOL_MAX) ) params = { "doorbot[settings][doorbell_volume]": str(value), } url = DOORBELLS_ENDPOINT.format(self.device_api_id) await self._ring.async_query(url, extra_params=params, method="PUT") @property def keep_alive_auto(self) -> float | None: """The keep alive auto setting.""" if self.kind in INTERCOM_KINDS: return self._attrs["settings"].get("keep_alive_auto") return None async def async_set_keep_alive_auto(self, value: float) -> None: """Update the keep alive auto setting.""" url = SETTINGS_ENDPOINT.format(self.device_api_id) payload = {"keep_alive_settings": {"keep_alive_auto": value}} await self._ring.async_query(url, method="PATCH", json=payload) @property def mic_volume(self) -> int | None: """Return mic volume.""" if self.kind in INTERCOM_KINDS: return self._attrs["settings"].get("mic_volume") return None async def async_set_mic_volume(self, value: int) -> None: """Set the mic volume.""" if not ((isinstance(value, int)) and (MIC_VOL_MIN <= value <= MIC_VOL_MAX)): raise RingError(MSG_VOL_OUTBOUND.format(MIC_VOL_MIN, MIC_VOL_MAX)) url = SETTINGS_ENDPOINT.format(self.device_api_id) payload = {"volume_settings": {"mic_volume": value}} await self._ring.async_query(url, method="PATCH", json=payload) @property def voice_volume(self) -> int | None: """Return voice volume.""" if self.kind in INTERCOM_KINDS: return self._attrs["settings"].get("voice_volume") return None async def async_set_voice_volume(self, value: int) -> None: """Set the voice volume.""" if not ((isinstance(value, int)) and (VOICE_VOL_MIN <= value <= VOICE_VOL_MAX)): raise RingError(MSG_VOL_OUTBOUND.format(VOICE_VOL_MIN, VOICE_VOL_MAX)) url = SETTINGS_ENDPOINT.format(self.device_api_id) payload = {"volume_settings": {"voice_volume": value}} await self._ring.async_query(url, method="PATCH", json=payload) async def async_get_clip_length_max(self) -> int | None: """Get the Maximum clip length.""" url = SETTINGS_ENDPOINT.format(self.device_api_id) resp = await self._ring.async_query(url, method="GET") return resp.json().get("video_settings", {}).get("clip_length_max") async def async_set_clip_length_max(self, value: int) -> None: """Set the maximum clip length. This value sets an effective refractory period on consecutive rigns eg if set to default value of 60, rings occuring with 60 seconds of first will not be detected. """ url = SETTINGS_ENDPOINT.format(self.device_api_id) payload = {"video_settings": {"clip_length_max": value}} await self._ring.async_query(url, method="PATCH", json=payload) @property def connection_status(self) -> str | None: """Return connection status.""" if self.kind in INTERCOM_KINDS: return self._attrs.get("alerts", {}).get("connection") return None async def async_get_allowed_users(self) -> list[dict[str, Any]] | None: """Return list of users allowed or invited to access.""" if self.kind in INTERCOM_KINDS: url = INTERCOM_ALLOWED_USERS.format(self.location_id) resp = await self._ring.async_query(url, method="GET") return resp.json() return None async def async_open_door(self, user_id: int = -1) -> bool: """Open the door.""" if self.kind in INTERCOM_KINDS: url = INTERCOM_OPEN_ENDPOINT.format(self.device_api_id) request_id = str(uuid.uuid4()) # params can also accept: # issue_time: in seconds # command_timeout: in seconds payload = { "command_name": "device_rpc", "request": { "id": request_id, "jsonrpc": "2.0", "method": "unlock_door", "params": { "door_id": 0, "user_id": user_id, }, }, } resp = await self._ring.async_query(url, method="PUT", json=payload) response = resp.json() if response.get("result", {}).get("code", -1) == 0: return True return False async def async_invite_access(self, email: str) -> bool: """Invite user.""" if self.kind in INTERCOM_KINDS: url = INTERCOM_INVITATIONS_ENDPOINT.format(self.location_id) payload = { "invitation": { "doorbot_ids": [self.device_api_id], "invited_email": email, "group_ids": [], } } await self._ring.async_query(url, method="POST", json=payload) return True return False async def async_remove_access(self, user_id: int) -> bool: """Remove user access or invitation.""" if self.kind in INTERCOM_KINDS: url = INTERCOM_INVITATIONS_DELETE_ENDPOINT.format(self.location_id, user_id) await self._ring.async_query(url, method="DELETE") return True return False DEPRECATED_API_QUERIES: ClassVar = { *RingGeneric.DEPRECATED_API_QUERIES, "update_health_data", "open_door", "invite_access", "remove_access", } DEPRECATED_API_PROPERTY_GETTERS: ClassVar = { *RingGeneric.DEPRECATED_API_PROPERTY_GETTERS, "clip_length_max", "allowed_users", } DEPRECATED_API_PROPERTY_SETTERS: ClassVar = { *RingGeneric.DEPRECATED_API_PROPERTY_SETTERS, "doorbell_volume", "keep_alive_auto", "mic_volume", "voice_volume", "clip_length_max", } python-ring-doorbell-0.9.13/ring_doorbell/py.typed000066400000000000000000000000001472135273200221760ustar00rootroot00000000000000python-ring-doorbell-0.9.13/ring_doorbell/ring.py000066400000000000000000000377721472135273200220420ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Doorbell module.""" from __future__ import annotations import logging import time from itertools import chain from typing import TYPE_CHECKING, Any, ClassVar from ring_doorbell import RingEvent from ring_doorbell.chime import RingChime from ring_doorbell.doorbot import RingDoorBell from ring_doorbell.exceptions import RingError from ring_doorbell.group import RingLightGroup from ring_doorbell.other import RingOther from ring_doorbell.stickup_cam import RingStickUpCam from .const import ( API_URI, API_VERSION, DEVICES_ENDPOINT, DINGS_ENDPOINT, GROUPS_ENDPOINT, INTERCOM_KINDS, NEW_SESSION_ENDPOINT, ) if TYPE_CHECKING: from collections.abc import Iterator, Mapping, Sequence from ring_doorbell.auth import Auth from ring_doorbell.generic import RingGeneric _logger = logging.getLogger(__name__) class Ring: """A Python Abstraction object to Ring Door Bell.""" def __init__(self, auth: Auth) -> None: """Initialize the Ring object.""" self.auth: Auth = auth self.session = None self.subscription = None self.devices_data: dict[str, dict[int, dict[str, Any]]] = {} self._devices: RingDevices | None = None self.chime_health_data = None self.doorbell_health_data = None self.dings_data: dict[Any, Any] = {} self.push_dings_data: list[RingEvent] = [] self.groups_data: dict[str, dict[str, Any]] = {} self.init_loop = None self.session_refresh_time: float | None = None async def async_update_data(self) -> None: """Update all data.""" await self._async_update_data() async def _async_update_data(self) -> None: if self.session is None: await self.async_create_session() await self.async_update_devices() await self.async_update_dings() await self.async_update_groups() def _add_event_to_dings_data(self, ring_event: RingEvent) -> None: # Purge expired push_dings now = time.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) async def async_create_session(self) -> None: """Create a new Ring session.""" session_post_data = { "device": { "hardware_id": self.auth.get_hardware_id(), "metadata": { "api_version": API_VERSION, "device_model": self.auth.get_device_model(), }, "os": "android", } } resp = await self._async_query( NEW_SESSION_ENDPOINT, method="POST", json=session_post_data, ) self.session = resp.json() self.session_refresh_time = time.monotonic() async def async_update_devices(self) -> None: """Update device data.""" if self.session is None: await self.async_create_session() resp = await self._async_query(DEVICES_ENDPOINT) data: dict[Any, Any] = resp.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() } async def async_update_dings(self) -> None: """Update dings data.""" if self.session is None: await self.async_create_session() resp = await self._async_query(DINGS_ENDPOINT) self.dings_data = resp.json() async def async_update_groups(self) -> None: """Update groups data.""" if self.session is None: await self.async_create_session() # Get all locations locations = set() devices = self.devices() for device_type in devices: for dev in devices[device_type]: if dev.location_id is not None: locations.add(dev.location_id) # Query for groups self.groups_data = {} for location in locations: resp = await self._async_query(GROUPS_ENDPOINT.format(location)) data = resp.json() if data["device_groups"]: for group in data["device_groups"]: self.groups_data[group["device_group_id"]] = group async def async_query( # noqa: PLR0913 self, url: str, method: str = "GET", extra_params: dict[str, Any] | None = None, data: bytes | None = None, json: dict[Any, Any] | None = None, timeout: float | None = None, base_uri: str = API_URI, ) -> Auth.Response: """Query data from Ring API.""" if self.session is None: await self.async_create_session() return await self._async_query( url, method, extra_params, data, json, timeout, base_uri ) async def _async_query( # noqa: PLR0913 self, url: str, method: str = "GET", extra_params: dict[str, Any] | None = None, data: bytes | None = None, json: dict[Any, Any] | None = None, timeout: float | None = None, base_uri: str = API_URI, ) -> Auth.Response: _logger.debug( "url: %s\nmethod: %s\njson: %s\ndata: %s\n extra_params: %s", url, method, json, data, extra_params, ) return await self.auth.async_query( base_uri + url, method=method, extra_params=extra_params, data=data, json=json, timeout=timeout, ) def devices(self) -> RingDevices: """Get all devices.""" if not self._devices: self._devices = RingDevices(self, self.devices_data) return self._devices def get_device_list(self) -> Sequence[RingGeneric]: """Get a combined list of all devices.""" devices = self.devices() return list( chain( devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"], devices["chimes"], devices["other"], ) ) def get_device_by_name(self, device_name: str) -> RingGeneric | None: """Return a device using it's name.""" all_devices = self.get_device_list() names_to_idx = {device.name: idx for (idx, device) in enumerate(all_devices)} return ( None if device_name not in names_to_idx else all_devices[names_to_idx[device_name]] ) def get_video_device_by_name(self, device_name: str) -> RingDoorBell | None: """Return a device using it's name.""" video_devices = self.video_devices() names_to_idx = {device.name: idx for (idx, device) in enumerate(video_devices)} return ( None if device_name not in names_to_idx else video_devices[names_to_idx[device_name]] ) def get_device_by_api_id(self, device_api_id: int) -> RingGeneric | None: """Return a device using it's id.""" all_devices = self.get_device_list() api_id_to_idx = { device.device_api_id: idx for (idx, device) in enumerate(all_devices) } return ( None if device_api_id not in api_id_to_idx else all_devices[api_id_to_idx[device_api_id]] ) def video_devices(self) -> Sequence[RingDoorBell]: """Get all devices.""" devices = self.devices() return list( chain(devices.doorbots, devices.authorized_doorbots, devices.stickup_cams) ) def groups(self) -> Mapping[str, RingLightGroup]: """Get all groups.""" groups = {} for group_id in self.groups_data: groups[group_id] = RingLightGroup(self, group_id) return groups def active_alerts(self) -> Sequence[RingEvent]: """Get active alerts.""" now = time.time() # Purge expired push_dings self.push_dings_data = [ re for re in self.push_dings_data if now < re.now + re.expires_in ] # Get unique id dictionary alerts: dict[tuple[int, int, str], RingEvent] = {} for re in self.push_dings_data: key = (re.doorbot_id, re.id, re.kind) if key not in alerts or re.now > alerts[key].now: alerts[key] = re for ding_data in self.dings_data: expires_at = ding_data.get("now") + ding_data.get("expires_in") if now < expires_at: re = RingEvent( id=ding_data["id"], doorbot_id=ding_data["doorbot_id"], device_name=ding_data["doorbot_description"], device_kind=ding_data["device_kind"], now=ding_data["now"], expires_in=ding_data["expires_in"], kind=ding_data["kind"], state=ding_data["state"], ) key = (re.doorbot_id, re.id, re.kind) if key not in alerts or re.now > alerts[key].now: alerts[key] = re return list(alerts.values()) DEPRECATED_API_QUERIES: ClassVar = { "update_devices", "update_data", "update_dings", "update_groups", "create_session", "query", } if not TYPE_CHECKING: def __getattr__(self, name: str) -> Any: """Get a deprecated attribute or raise an error.""" if name in self.DEPRECATED_API_QUERIES: return self.auth._dep_handler.get_api_query(self, name) # noqa: SLF001 msg = f"{self.__class__.__name__} has no attribute {name!r}" raise AttributeError(msg) class RingDevices: """Class to represent collection of devices.""" def __init__( self, ring: Ring, devices_data: dict[str, dict[int, dict[str, Any]]] ) -> None: """Initialise the devices from the api response.""" self._stickup_cams: list[RingStickUpCam] = [] self._chimes: list[RingChime] = [] self._doorbots: list[RingDoorBell] = [] self._authorized_doorbots: list[RingDoorBell] = [] self._other: list[RingOther] = [] for device_type, devices in devices_data.items(): if device_type == "stickup_cams": self._stickup_cams = [ RingStickUpCam(ring, device_id) for device_id in devices ] if device_type == "chimes": self._chimes = [RingChime(ring, device_id) for device_id in devices] if device_type == "doorbots": self._doorbots = [ RingDoorBell(ring, device_id) for device_id in devices ] if device_type == "authorized_doorbots": self._authorized_doorbots = [ RingDoorBell(ring, device_id, shared=True) for device_id in devices ] if device_type == "other": self._other = [ RingOther(ring, device_id, shared=True) for device_id, device in devices.items() if (device_kind := device.get("kind")) and device_kind in INTERCOM_KINDS ] self._all_devices = { device.id: device for device in chain( self._stickup_cams, self._chimes, self._doorbots, self._authorized_doorbots, self._other, ) } def __getitem__(self, device_type: str) -> Sequence[RingGeneric]: """Get a generic device by type.""" if device_type == "stickup_cams": return self._stickup_cams if device_type == "chimes": return self._chimes if device_type == "doorbots": return self._doorbots if device_type == "authorized_doorbots": return self._authorized_doorbots if device_type == "other": return self._other if device_type == "intercoms": return self._other msg = f"Invalid device_type {device_type}" raise RingError(msg) def __iter__(self) -> Iterator[str]: """Device type iterator.""" return iter( ["stickup_cams", "chimes", "doorbots", "authorized_doorbots", "other"] ) @property def stickup_cams(self) -> Sequence[RingStickUpCam]: """The stickup cams.""" return self._stickup_cams @property def chimes(self) -> Sequence[RingChime]: """The chimes.""" return self._chimes @property def doorbots(self) -> Sequence[RingDoorBell]: """The doorbots.""" return self._doorbots @property def authorized_doorbots(self) -> Sequence[RingDoorBell]: """The authorized_doorbots.""" return self._authorized_doorbots @property def doorbells(self) -> Sequence[RingDoorBell]: """The doorbells, i.e. doorbots and authorized_doorbots combined.""" return self._doorbots + self._authorized_doorbots @property def other(self) -> Sequence[RingOther]: """The other devices, i.e. intercoms.""" return self._other @property def all_devices(self) -> Sequence[RingGeneric]: """All devices combined.""" return list(self._all_devices.values()) @property def video_devices(self) -> Sequence[RingDoorBell]: """The video devices, i.e. doorbells and stickup_cams.""" return [*self._doorbots, *self._authorized_doorbots, *self._stickup_cams] def get_device(self, device_api_id: int) -> RingGeneric: """Get device by api id.""" if device := self._all_devices.get(device_api_id): return device msg = f"device with id {device_api_id} not found" raise RingError(msg) def get_doorbell(self, device_api_id: int) -> RingDoorBell: """Get doorbell by api id.""" if ( (device := self._all_devices.get(device_api_id)) and isinstance(device, RingDoorBell) and not issubclass(device.__class__, RingDoorBell) ): return device msg = f"doorbell with id {device_api_id} not found" raise RingError(msg) def get_stickup_cam(self, device_api_id: int) -> RingStickUpCam: """Get stickup_cam by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingStickUpCam ): return device msg = f"stickup_cam with id {device_api_id} not found" raise RingError(msg) def get_chime(self, device_api_id: int) -> RingChime: """Get chime by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingChime ): return device msg = f"chime with id {device_api_id} not found" raise RingError(msg) def get_other(self, device_api_id: int) -> RingOther: """Get other device by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingOther ): return device msg = f"other device with id {device_api_id} not found" raise RingError(msg) def get_video_device(self, device_api_id: int) -> RingDoorBell: """Get video capable device by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingDoorBell ): return device msg = f"video capable device with id {device_api_id} not found" raise RingError(msg) def __str__(self) -> str: """Get string representation of devices.""" d = {dev_type: self.__getitem__(dev_type) for dev_type in self.__iter__()} return "{" + "\n".join(f"{k!r}: {v!r}," for k, v in d.items()) + "}" def __repr__(self) -> str: """Return repr of devices.""" d = {dev_type: self.__getitem__(dev_type) for dev_type in self.__iter__()} return repr(d) python-ring-doorbell-0.9.13/ring_doorbell/stickup_cam.py000066400000000000000000000151741472135273200233750ustar00rootroot00000000000000# vim:sw=4:ts=4:et: """Python Ring Doorbell wrapper.""" from __future__ import annotations import logging from typing import ClassVar from ring_doorbell.const import ( FLOODLIGHT_CAM_KINDS, FLOODLIGHT_CAM_PLUS_KINDS, FLOODLIGHT_CAM_PRO_KINDS, INDOOR_CAM_GEN2_KINDS, INDOOR_CAM_KINDS, LIGHTS_ENDPOINT, MSG_ALLOWED_VALUES, MSG_VOL_OUTBOUND, SIREN_DURATION_MAX, SIREN_DURATION_MIN, SIREN_ENDPOINT, SPOTLIGHT_CAM_BATTERY_KINDS, SPOTLIGHT_CAM_PLUS_KINDS, SPOTLIGHT_CAM_PRO_KINDS, SPOTLIGHT_CAM_WIRED_KINDS, STICKUP_CAM_BATTERY_KINDS, STICKUP_CAM_ELITE_KINDS, STICKUP_CAM_GEN3_KINDS, STICKUP_CAM_KINDS, RingCapability, ) from ring_doorbell.doorbot import RingDoorBell from ring_doorbell.exceptions import RingError _LOGGER = logging.getLogger(__name__) class RingStickUpCam(RingDoorBell): """Implementation for RingStickUpCam.""" @property def family(self) -> str: """Return Ring device family type.""" return "stickup_cams" @property def model(self) -> str: # noqa: C901, PLR0911, PLR0912 """Return Ring device model name.""" if self.kind in FLOODLIGHT_CAM_KINDS: return "Floodlight Cam" if self.kind in FLOODLIGHT_CAM_PRO_KINDS: return "Floodlight Cam Pro" if self.kind in FLOODLIGHT_CAM_PLUS_KINDS: return "Floodlight Cam Plus" if self.kind in INDOOR_CAM_KINDS: return "Indoor Cam" if self.kind in INDOOR_CAM_GEN2_KINDS: return "Indoor Cam (2nd Gen)" if self.kind in SPOTLIGHT_CAM_BATTERY_KINDS: return "Spotlight Cam {}".format( self._attrs.get("ring_cam_setup_flow", "battery").title() ) if self.kind in SPOTLIGHT_CAM_WIRED_KINDS: return "Spotlight Cam {}".format( self._attrs.get("ring_cam_setup_flow", "wired").title() ) if self.kind in SPOTLIGHT_CAM_PLUS_KINDS: return "Spotlight Cam Plus" if self.kind in SPOTLIGHT_CAM_PRO_KINDS: return "Spotlight Cam Pro" if self.kind in STICKUP_CAM_KINDS: return "Stick Up Cam" if self.kind in STICKUP_CAM_BATTERY_KINDS: return "Stick Up Cam Battery" if self.kind in STICKUP_CAM_ELITE_KINDS: return "Stick Up Cam Wired" if self.kind in STICKUP_CAM_GEN3_KINDS: return "Stick Up Cam (3rd Gen)" _LOGGER.error("Unknown kind: %s", self.kind) return "Unknown Stickup Cam" def has_capability(self, capability: RingCapability | str) -> bool: """Return if device has specific capability.""" capability = ( capability if isinstance(capability, RingCapability) else RingCapability.from_name(capability) ) if capability == RingCapability.HISTORY: return True if capability == RingCapability.BATTERY: return self.kind in ( SPOTLIGHT_CAM_BATTERY_KINDS + STICKUP_CAM_KINDS + STICKUP_CAM_BATTERY_KINDS + STICKUP_CAM_GEN3_KINDS ) if capability == RingCapability.LIGHT: return self.kind in ( FLOODLIGHT_CAM_KINDS + FLOODLIGHT_CAM_PRO_KINDS + FLOODLIGHT_CAM_PLUS_KINDS + SPOTLIGHT_CAM_BATTERY_KINDS + SPOTLIGHT_CAM_WIRED_KINDS + SPOTLIGHT_CAM_PLUS_KINDS + SPOTLIGHT_CAM_PRO_KINDS ) if capability == RingCapability.SIREN: return self.kind in ( FLOODLIGHT_CAM_KINDS + FLOODLIGHT_CAM_PRO_KINDS + FLOODLIGHT_CAM_PLUS_KINDS + INDOOR_CAM_KINDS + INDOOR_CAM_GEN2_KINDS + SPOTLIGHT_CAM_BATTERY_KINDS + SPOTLIGHT_CAM_WIRED_KINDS + SPOTLIGHT_CAM_PLUS_KINDS + SPOTLIGHT_CAM_PRO_KINDS + STICKUP_CAM_BATTERY_KINDS + STICKUP_CAM_ELITE_KINDS + STICKUP_CAM_GEN3_KINDS ) if capability in [RingCapability.MOTION_DETECTION, RingCapability.VIDEO]: return self.kind in ( FLOODLIGHT_CAM_KINDS + FLOODLIGHT_CAM_PRO_KINDS + FLOODLIGHT_CAM_PLUS_KINDS + INDOOR_CAM_KINDS + INDOOR_CAM_GEN2_KINDS + SPOTLIGHT_CAM_BATTERY_KINDS + SPOTLIGHT_CAM_WIRED_KINDS + SPOTLIGHT_CAM_PLUS_KINDS + SPOTLIGHT_CAM_PRO_KINDS + STICKUP_CAM_KINDS + STICKUP_CAM_BATTERY_KINDS + STICKUP_CAM_ELITE_KINDS + STICKUP_CAM_GEN3_KINDS ) return False @property def lights(self) -> str: """Return lights status.""" return self._attrs.get("led_status", "") async def async_set_lights(self, state: str) -> None: """Control the lights.""" values = ["on", "off"] if state not in values: raise RingError(MSG_ALLOWED_VALUES.format(", ".join(values))) url = LIGHTS_ENDPOINT.format(self.device_api_id, state) await self._ring.async_query(url, method="PUT") @property def light(self) -> bool: """Return lights status.""" return self._attrs.get("led_status", "") == "on" async def async_set_light(self, value: bool) -> None: # noqa: FBT001 """Control the lights.""" state = "on" if value else "off" url = LIGHTS_ENDPOINT.format(self.device_api_id, state) await self._ring.async_query(url, method="PUT") @property def siren(self) -> int: """Return siren status.""" if siren_status := self._attrs.get("siren_status"): return siren_status.get("seconds_remaining", 0) return 0 async def async_set_siren(self, duration: int) -> None: """Control the siren.""" if not ( (isinstance(duration, int)) and (SIREN_DURATION_MIN <= duration <= SIREN_DURATION_MAX) ): raise RingError( MSG_VOL_OUTBOUND.format(SIREN_DURATION_MIN, SIREN_DURATION_MAX) ) if duration > 0: state = "on" params = {"duration": duration} else: state = "off" params = {} url = SIREN_ENDPOINT.format(self.device_api_id, state) await self._ring.async_query(url, extra_params=params, method="PUT") DEPRECATED_API_PROPERTY_SETTERS: ClassVar = { *RingDoorBell.DEPRECATED_API_PROPERTY_SETTERS, "lights", "siren", } python-ring-doorbell-0.9.13/ring_doorbell/util.py000066400000000000000000000121021472135273200220340ustar00rootroot00000000000000"""Module for common utility functions.""" from __future__ import annotations import asyncio import datetime import logging from contextlib import suppress from functools import update_wrapper from threading import Lock from typing import TYPE_CHECKING, Any, Callable from warnings import warn from typing_extensions import ParamSpec, TypeVar from ring_doorbell.exceptions import RingError if TYPE_CHECKING: from collections.abc import Coroutine from .auth import Auth from .generic import RingGeneric from .group import RingLightGroup from .listen.eventlistener import RingEventListener from .ring import Ring _T = TypeVar( "_T", bound=Auth | Ring | RingGeneric | RingLightGroup | RingEventListener ) _R = TypeVar("_R") _P = ParamSpec("_P") _logger = logging.getLogger(__name__) def parse_datetime(datetime_str: str) -> datetime.datetime: """Parse a datetime string into a datetime object. Ring api has inconsistent datetime string patterns. """ # Check if the datetime string contains a period which precedes 'Z', # indicating microseconds if "." in datetime_str and datetime_str.endswith("Z"): # String contains microseconds and ends with 'Z' format_str = "%Y-%m-%dT%H:%M:%S.%fZ" else: # String does not contain microseconds, should end with 'Z' # Could be updated to handle other formats format_str = "%Y-%m-%dT%H:%M:%SZ" try: res = datetime.datetime.strptime(datetime_str, format_str).replace( tzinfo=datetime.timezone.utc ) except ValueError: _logger.exception( "Unable to parse datetime string %s, defaulting to now time", datetime_str ) res = datetime.datetime.now(datetime.timezone.utc) return res class _DeprecatedSyncApiHandler: def __init__(self, auth: Auth) -> None: self.auth = auth self._sync_lock = Lock() async def run_and_close_session( self, async_method: Callable[_P, Coroutine[Any, Any, _R]], *args: _P.args, **kwargs: _P.kwargs, ) -> _R: try: self._sync_lock.acquire() res = await async_method(*args, **kwargs) finally: with suppress(Exception): await self.auth.async_close() self._sync_lock.release() return res @staticmethod def check_no_loop(classname: str, method_name: str) -> None: current_loop = None with suppress(RuntimeError): current_loop = asyncio.get_running_loop() if current_loop: msg = ( f"You cannot call deprecated sync function {classname}.{method_name} " "from within a running event loop." ) raise RingError(msg) def get_api_query( self, class_instance: _T, method_name: str, ) -> Any: """Return deprecated sync api query attribute.""" classname = type(class_instance).__name__ def _deprecated_sync_function( async_func: Callable[_P, Coroutine[Any, Any, _R]], ) -> Callable[_P, _R]: def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: self.check_no_loop(classname, method_name) msg = ( f"{classname}.{method_name} is deprecated, use " f"{classname}.{async_method_name}" ) warn(msg, DeprecationWarning, stacklevel=1) return asyncio.run( self.run_and_close_session(async_func, *args, **kwargs) ) return update_wrapper(wrapper, async_func) async_method_name = f"async_{method_name}" async_method = getattr(class_instance, async_method_name) return _deprecated_sync_function(async_method) def get_api_property( self, class_instance: _T, method_name: str, ) -> Any: """Return deprecated sync api property value.""" classname = type(class_instance).__name__ self.check_no_loop(classname, method_name) async_method_name = f"async_get_{method_name}" msg = ( f"{classname}.{method_name} is deprecated, use " f"{classname}.{async_method_name}" ) warn(msg, DeprecationWarning, stacklevel=1) async_method = getattr(class_instance, async_method_name) return asyncio.run(self.run_and_close_session(async_method)) def set_api_property( self, class_instance: _T, property_name: str, value: Any, ) -> None: """Set sync api property value.""" classname = type(class_instance).__name__ self.check_no_loop(classname, property_name) async_method_name = f"async_set_{property_name}" msg = ( f"{classname}.{property_name} is deprecated, use " f"{classname}.{async_method_name}" ) warn(msg, DeprecationWarning, stacklevel=1) async_method = getattr(class_instance, async_method_name) asyncio.run(self.run_and_close_session(async_method, value)) python-ring-doorbell-0.9.13/ring_doorbell/webrtcstream.py000066400000000000000000000337321472135273200235750ustar00rootroot00000000000000"""Python Ring Doorbell RTC Stream handler. This module is currently experimental and requires a webrtc enabled client to function. """ from __future__ import annotations import asyncio import contextlib import logging import ssl import time import uuid from dataclasses import dataclass from json import dumps as json_dumps from json import loads as json_loads from typing import TYPE_CHECKING, Any, Callable from async_timeout import timeout as asyncio_timeout from typing_extensions import TypeAlias from websockets.asyncio.client import connect from ring_doorbell.const import ( APP_API_URI, RTC_STREAMING_TICKET_ENDPOINT, RTC_STREAMING_WEB_SOCKET_ENDPOINT, ) from ring_doorbell.exceptions import RingError if TYPE_CHECKING: from collections.abc import Coroutine from websockets.asyncio.client import ClientConnection from .ring import Ring _LOGGER = logging.getLogger(__name__) SDP_ANSWER_TIMEOUT = 1 @dataclass class RingWebRtcMessage: """Class for async on_message callback.""" answer: str | None = None candidate: str | None = None sdp_m_line_index: int | None = None error_code: str | None = None error_message: str | None = None session_id: str | None = None RingWebRtcMessageCallback: TypeAlias = Callable[[RingWebRtcMessage], None] class RingWebRtcStream: """Class to handle a Web RTC Stream.""" PING_TIME_SECONDS = 5 def __init__( self, ring: Ring, device_api_id: int, *, keep_alive_timeout: int | None = 30, on_message_callback: RingWebRtcMessageCallback | None = None, on_close_callback: Callable[[], Coroutine[Any, Any, None]] | None = None, ) -> None: """Initialise the class.""" self._ring = ring self.device_api_id = device_api_id self.sdp: str | None = None self.websocket: ClientConnection | None = None self.is_alive = True self.ping_task: asyncio.Task | None = None self.read_task: asyncio.Task | None = None self._close_task: asyncio.Task | None = None self.ice_candidates: dict[int, list[str]] = {0: [], 1: [], 2: [], 3: []} self.collect_ice_candidates = False self.ssl_context: ssl.SSLContext | None = None self._sdp_answer_event = asyncio.Event() self._keep_alive_timeout = keep_alive_timeout self._last_keep_alive: float | None = None self._on_close_callback = on_close_callback self._on_message_callback = on_message_callback self.session_id: str | None = None self._offered_event = asyncio.Event() @staticmethod def get_sdp_session_id(sdp_offer: str) -> str | None: """Return the sdp session id from the offer.""" try: lines = sdp_offer.split("\n") for line in lines: if line[0] == "o": origin = line.split("=")[1] break return origin.split(" ")[1] except Exception: _LOGGER.exception("Error getting session id from offer: %s", sdp_offer) return None async def generate(self, sdp_offer: str) -> str | None: """Generate the RTC stream.""" await self._generate(sdp_offer) if self._on_message_callback: return None if self.collect_ice_candidates: _LOGGER.debug( "Waiting %s seconds for ice candidates", SDP_ANSWER_TIMEOUT, ) await asyncio.sleep(SDP_ANSWER_TIMEOUT) self.insert_ice_candidates() else: async with asyncio_timeout(SDP_ANSWER_TIMEOUT): await self._sdp_answer_event.wait() if not self.sdp: exmsg = "Unable to generate RTC stream in time" await self.close() raise RingError(exmsg) _LOGGER.debug("Returning SDP answer: %s", self.sdp) return self.sdp async def _generate(self, sdp_offer: str) -> None: """Generate the RTC stream.""" try: _LOGGER.debug("Generating stream with sdp offer: %s", sdp_offer) req = await self._ring.async_query( RTC_STREAMING_TICKET_ENDPOINT, method="POST", base_uri=APP_API_URI, ) ticket = req.json()["ticket"] _LOGGER.debug( "Received RTC streaming ticket %s from endpoint, creating websocket", ticket, ) ws_uri = RTC_STREAMING_WEB_SOCKET_ENDPOINT.format(uuid.uuid4(), ticket) loop = asyncio.get_running_loop() if not self.ssl_context: # create_default_context() blocks the event loop self.ssl_context = await loop.run_in_executor( None, ssl.create_default_context ) self.websocket = await connect( ws_uri, user_agent_header="android:com.ringapp", ssl=self.ssl_context, ) self.dialog_id = str(uuid.uuid4()) offer_msg = { "method": "live_view", "dialog_id": self.dialog_id, "body": { "doorbot_id": self.device_api_id, "stream_options": {"audio_enabled": True, "video_enabled": True}, "sdp": sdp_offer, "type": "offer", }, } _LOGGER.debug( "Connected to RTC streaming websocket, sending live_view offer msg: %s", offer_msg, ) _LOGGER.debug("Starting reader task") self.read_task = asyncio.create_task(self.reader()) await self.websocket.send(json_dumps(offer_msg)) self._offered_event.set() except Exception as ex: exmsg = "Error generating RTC stream" raise RingError(exmsg, ex) from ex async def _activate(self) -> None: if TYPE_CHECKING: assert self.websocket activate_msg = self.get_session_message("activate_session", {}) _LOGGER.debug("Sending activate_session message: %s", activate_msg) await self.websocket.send(json_dumps(activate_msg)) self._last_keep_alive = time.time() self.ping_task = asyncio.create_task(self.pinger()) async def on_ice_candidate(self, candidate: str, m_line_index: int) -> None: """Send an ICE candidate.""" async with asyncio_timeout(10): await self._offered_event.wait() assert self.websocket # noqa: S101 body = { "doorbot_id": self.device_api_id, "ice": candidate, "mlineindex": m_line_index, } # Set the session if it's been created. if self.session_id: body["session_id"] = self.session_id ice_msg = { "method": "ice", "dialog_id": self.dialog_id, "body": body, } _LOGGER.debug( "Sending ice candidate for mlineindex %s: %s", m_line_index, candidate ) await self.websocket.send(json_dumps(ice_msg)) async def keep_alive(self) -> None: """Keep alive the rtc stream.""" self._last_keep_alive = time.time() def get_session_message(self, method: str, body: dict[str, Any]) -> dict[str, Any]: """Get a message to send to the session.""" return { "method": method, "dialog_id": self.dialog_id, "body": { **body, "doorbot_id": self.device_api_id, "session_id": self.session_id, }, } async def reader(self) -> None: """Read messages from the websocket.""" if TYPE_CHECKING: assert self.websocket async for message in self.websocket: if TYPE_CHECKING: assert isinstance(message, str) await self.handle_message(message) async def pinger(self) -> None: """Ping to keep the session alive.""" if TYPE_CHECKING: assert self.websocket assert self._last_keep_alive while self.is_alive and ( self._keep_alive_timeout is None or (time.time() - self._last_keep_alive) <= self._keep_alive_timeout ): await asyncio.sleep(self.PING_TIME_SECONDS) ping = self.get_session_message("ping", {}) await self.websocket.send(json_dumps(ping)) def handle_ice_message(self, message: dict) -> None: """Handle an ice candidate message.""" ice_candidate = message["body"]["ice"] multi_line_index = message["body"]["mlineindex"] if self._on_message_callback: ice_message = RingWebRtcMessage( candidate=ice_candidate, sdp_m_line_index=multi_line_index ) self._on_message_callback(ice_message) elif self.collect_ice_candidates: _LOGGER.debug( "Ice candidate received, multi_line_index: %s candidate: %s", multi_line_index, ice_candidate, ) self.ice_candidates[int(multi_line_index)].append(ice_candidate) async def handle_answer_message(self, message: dict) -> None: """Handle an sdp answer message.""" sdp = message["body"]["sdp"] _LOGGER.debug("SDP answer received: %s", sdp) self.sdp = sdp self._sdp_answer_event.set() if self._on_message_callback: answer_message = RingWebRtcMessage(answer=sdp) self._on_message_callback(answer_message) await self._activate() async def handle_close_message(self, message: dict) -> None: """Handle an sdp answer message.""" reason = message["body"]["reason"] reason_code = reason["code"] reason_message = reason["text"] _LOGGER.debug("Close message received: %s", str(reason)) self.is_alive = False await self._close(closed_by_self=True) if self._on_message_callback: error_message = RingWebRtcMessage( error_code=reason_code, error_message=reason_message ) self._on_message_callback(error_message) def insert_ice_candidates(self) -> None: """Insert an ice candidate into the sdp answer. In 2023 the ring api did not return the ice servers in the sdp answer and they had to be added as they were received on the web socket. As of Sep 2024 they are coming back with the initial sdp answer. """ if TYPE_CHECKING: assert self.sdp _LOGGER.debug("Inserting ICE candidates into sdp answer") self.collect_ice_candidates = False for line_index, candidates in self.ice_candidates.items(): if not candidates: continue candidates_dict = { int(candidate[10:12]): f"a={candidate}\r\n" for candidate in candidates } candidates_text = ("").join(dict(sorted(candidates_dict.items())).values()) multi_text = f"a=mid:{line_index}" self.sdp = self.sdp.replace(multi_text, candidates_text + multi_text) def sync_close(self) -> None: """Close a WebRTC session.""" if self.is_alive and not self._close_task: self._close_task = asyncio.create_task(self.close()) async def close(self) -> None: """Close the rtc stream.""" _LOGGER.debug("Closing the RTC Stream") await self._close(closed_by_self=False) self._close_task = None async def _close(self, *, closed_by_self: bool) -> None: """Close the stream.""" self.session_id = None if closed_by_self and (close_cb := self._on_close_callback): self._on_close_callback = None await close_cb() self.is_alive = False if ping_task := self.ping_task: self.ping_task = None if not ping_task.done(): ping_task.cancel() with contextlib.suppress(asyncio.CancelledError): await ping_task if websocket := self.websocket: self.websocket = None await websocket.close() if read_task := self.read_task: self.read_task = None if not read_task.done(): await read_task async def handle_message(self, message_str: str) -> None: # noqa: C901, PLR0912 """Handle a message from the web socket.""" if TYPE_CHECKING: assert self.websocket message = json_loads(message_str) method = message["method"] if method == "ice": self.handle_ice_message(message) elif method == "sdp": await self.handle_answer_message(message) elif method == "notification": text = message["body"]["text"] if text == "camera_connected": _LOGGER.debug("Notification received: %s", text) camera_options = self.get_session_message( "camera_options", {"stealth_mode": False} ) _LOGGER.debug("Sending camera options: %s", camera_options) await self.websocket.send(json_dumps(camera_options)) else: _LOGGER.debug("Received notification: %s", message) elif method == "session_created": self.session_id = message["body"]["session_id"] if TYPE_CHECKING: assert self.session_id _LOGGER.debug( "Session created: %s___%s", self.session_id[:16], self.session_id[-16:] ) elif method == "close": await self.handle_close_message(message) elif method == "pong": _LOGGER.debug("Pong message received") elif method == "camera_started": _LOGGER.debug("camera_started message received") elif method == "camera_options": _LOGGER.debug("camera_options message received: %s", message["body"]) else: _LOGGER.debug("Unknown message received with method: %s", method) python-ring-doorbell-0.9.13/test.py000066400000000000000000000025771472135273200172340ustar00rootroot00000000000000"""Test module which runs the first example in the README.""" import asyncio import getpass import json from pathlib import Path from pprint import pprint from ring_doorbell import Auth, AuthenticationError, Requires2FAError, Ring user_agent = "YourProjectName-1.0" # Change this cache_file = Path(user_agent + ".token.cache") def token_updated(token) -> None: cache_file.write_text(json.dumps(token)) def otp_callback(): return input("2FA code: ") async def do_auth(): username = input("Username: ") password = getpass.getpass("Password: ") auth = Auth(user_agent, None, token_updated) try: await auth.async_fetch_token(username, password) except Requires2FAError: await auth.async_fetch_token(username, password, otp_callback()) return auth async def main() -> None: if cache_file.is_file(): # auth token is cached auth = Auth(user_agent, json.loads(cache_file.read_text()), token_updated) ring = Ring(auth) try: await ring.async_create_session() # auth token still valid except AuthenticationError: # auth token has expired auth = await do_auth() else: auth = await do_auth() # Get new auth token ring = Ring(auth) await ring.async_update_data() print(ring.devices()) await auth.async_close() if __name__ == "__main__": asyncio.run(main()) python-ring-doorbell-0.9.13/test_sync.py000066400000000000000000000023441472135273200202600ustar00rootroot00000000000000"""Test module which runs the first example in the README.""" import getpass import json from pathlib import Path from ring_doorbell import Auth, AuthenticationError, Requires2FAError, Ring user_agent = "YourProjectName-1.0" # Change this cache_file = Path(user_agent + ".token.cache") def token_updated(token) -> None: cache_file.write_text(json.dumps(token)) def otp_callback(): return input("2FA code: ") def do_auth(): username = input("Username: ") password = getpass.getpass("Password: ") auth = Auth(user_agent, None, token_updated) try: auth.fetch_token(username, password) except Requires2FAError: auth.fetch_token(username, password, otp_callback()) return auth def main() -> None: if cache_file.is_file(): # auth token is cached auth = Auth(user_agent, json.loads(cache_file.read_text()), token_updated) ring = Ring(auth) try: ring.create_session() # auth token still valid except AuthenticationError: # auth token has expired auth = do_auth() else: auth = do_auth() # Get new auth token ring = Ring(auth) ring.update_data() print(ring.devices()) if __name__ == "__main__": main() python-ring-doorbell-0.9.13/tests/000077500000000000000000000000001472135273200170325ustar00rootroot00000000000000python-ring-doorbell-0.9.13/tests/__init__.py000066400000000000000000000000531472135273200211410ustar00rootroot00000000000000"""Tests for Ring Door Bell components.""" python-ring-doorbell-0.9.13/tests/conftest.py000066400000000000000000000255771472135273200212510ustar00rootroot00000000000000"""Test configuration for the Ring platform.""" from __future__ import annotations import datetime import json import re from pathlib import Path from time import time from typing import TYPE_CHECKING from unittest.mock import patch import pytest from aioresponses import CallbackResult, aioresponses from ring_doorbell import Auth, Ring from ring_doorbell.const import USER_AGENT if TYPE_CHECKING: from collections.abc import Generator # The kwargs below are useful for request assertions def json_request_kwargs(): return { "headers": { "User-Agent": "android:com.ringapp", "hardware_id": "21ac3af1-0eac-5fbd-8b0f-0b784889bfbd", "Content-Type": "application/json", "Authorization": "Bearer dummyBearerToken", }, "timeout": 10, "data": None, "params": {}, "json": {}, } def nojson_request_kwargs(): return { "headers": { "User-Agent": "android:com.ringapp", "hardware_id": "21ac3af1-0eac-5fbd-8b0f-0b784889bfbd", "Authorization": "Bearer dummyBearerToken", }, "timeout": 10, "data": None, "params": {}, } def pytest_configure(config): config.addinivalue_line( "markers", "nolistenmock: mark test to not want the autouse listenmock" ) @pytest.fixture async def auth(): """Return auth object.""" auth = Auth(USER_AGENT) await auth.async_fetch_token("foo", "bar") yield auth await auth.async_close() @pytest.fixture async def ring(auth): """Return updated ring object.""" ring = Ring(auth) await ring.async_update_data() return ring def _set_dings_to_now(active_dings) -> None: for ding in active_dings: ding["now"] = time() return active_dings def load_fixture(filename): """Load a fixture.""" path = Path(Path(__file__).parent / "fixtures" / filename) with path.open() as fdp: return fdp.read() def load_fixture_as_dict(filename): """Load a fixture.""" return json.loads(load_fixture(filename)) def load_alert_v1( alert_type: str, device_id, *, ding_id_inc: int = 0, created_at: str | None = None ) -> dict: msg = json.loads(load_fixture(Path().joinpath("listen", "fcmdata_v1.json"))) gcmdata = json.loads( load_fixture(Path().joinpath("listen", f"{alert_type}_gcmdata.json")) ) if created_at is None: created_at = datetime.datetime.utcnow().isoformat(timespec="milliseconds") + "Z" # noqa: DTZ003 if "ding" in gcmdata: gcmdata["ding"]["doorbot_id"] = device_id gcmdata["ding"]["created_at"] = created_at gcmdata["ding"]["id"] = gcmdata["ding"]["id"] + ding_id_inc else: gcmdata["alarm_meta"]["device_zid"] = device_id msg["data"]["gcmData"] = json.dumps(gcmdata) return msg def load_alert_v2( alert_type: str, device_id, *, ding_id_inc: int = 0, created_at: str | None = None ) -> dict: msg = json.loads(load_fixture(Path().joinpath("listen", "fcmdata_v2.json"))) data = json.loads( load_fixture(Path().joinpath("listen", f"{alert_type}_data.json")) ) android_config = json.loads( load_fixture(Path().joinpath("listen", f"{alert_type}_android_config.json")) ) analytics = json.loads( load_fixture(Path().joinpath("listen", f"{alert_type}_analytics.json")) ) if created_at is None: created_at = datetime.datetime.utcnow().isoformat(timespec="milliseconds") + "Z" # noqa: DTZ003 data["device"]["id"] = device_id data["event"]["ding"]["created_at"] = created_at data["event"]["ding"]["id"] = str(int(data["event"]["ding"]["id"]) + ding_id_inc) msg["data"]["data"] = json.dumps(data) msg["data"]["android_config"] = json.dumps(android_config) msg["data"]["analytics"] = json.dumps(analytics) return msg @pytest.fixture(autouse=True) def _listen_mock(mocker, request) -> None: if "nolistenmock" in request.keywords: return mocker.patch( "firebase_messaging.FcmPushClient.checkin_or_register", return_value="foobar" ) mocker.patch("firebase_messaging.FcmPushClient.start") mocker.patch("firebase_messaging.FcmPushClient.stop") mocker.patch("firebase_messaging.FcmPushClient.is_started", return_value=True) def callback(url, **kwargs): # noqa: ANN003 return CallbackResult(status=418) # tests to pull in request_mock and append uris @pytest.fixture def devices_fixture(): class Devices: def __init__(self) -> None: """Initialise the class.""" self.updated = False def devices(self) -> dict: """Get the devices.""" if not self.updated: return load_fixture_as_dict("ring_devices.json") return load_fixture_as_dict("ring_devices_updated.json") def callback(self, url, **kwargs) -> CallbackResult: # noqa: ARG002, ANN003 """Return the callback result.""" return CallbackResult(payload=self.devices()) return Devices() @pytest.fixture def putpatch_status_fixture(): class StatusOverrides: def __init__(self) -> None: """Initialise the class.""" self.overrides = {} def callback(self, url, **kwargs) -> CallbackResult: # noqa: ANN003, ARG002 """Return the callback.""" plain_url = str(url) if plain_url in self.overrides: return CallbackResult(body=b"", status=self.overrides[plain_url]) return CallbackResult(body=b"", status=204) return StatusOverrides() @pytest.fixture(autouse=True, name="hardware_id_mock") def _hardware_id_mock_fixture() -> Generator: """Fixture to patch getnode ensures all tests generate same hardware_id.""" with patch("uuid.getnode", return_value=12345678901): yield # setting the fixture name to requests_mock allows other # tests to pull in request_mock and append uris @pytest.fixture(autouse=True, name="aioresponses_mock") def aioresponses_mock_fixture(request, devices_fixture, putpatch_status_fixture): with aioresponses() as mock: mock.post( "https://oauth.ring.com/oauth/token", payload=load_fixture_as_dict("ring_oauth.json"), repeat=True, ) mock.post( "https://api.ring.com/clients_api/session", payload=load_fixture_as_dict("ring_session.json"), repeat=True, ) mock.get( "https://api.ring.com/clients_api/ring_devices", callback=devices_fixture.callback, repeat=True, ) mock.get( re.compile(r"https:\/\/api\.ring\.com\/clients_api\/chimes\/\d+\/health"), payload=load_fixture_as_dict("ring_chime_health_attrs.json"), repeat=True, ) mock.get( re.compile(r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/health"), payload=load_fixture_as_dict("ring_doorboot_health_attrs.json"), repeat=True, ) mock.get( re.compile( r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/185036587\/history.*$" ), payload=load_fixture_as_dict("ring_intercom_history.json"), repeat=True, ) mock.get( re.compile( r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/history.*$" ), payload=load_fixture_as_dict("ring_doorbot_history.json"), repeat=True, ) mock.get( "https://api.ring.com/clients_api/dings/active", payload=_set_dings_to_now(load_fixture_as_dict("ring_ding_active.json")), repeat=True, ) mock.put( "https://api.ring.com/clients_api/doorbots/987652/floodlight_light_off", payload="ok", repeat=True, ) mock.put( "https://api.ring.com/clients_api/doorbots/987652/floodlight_light_on", payload="ok", repeat=True, ) mock.put( "https://api.ring.com/clients_api/doorbots/987652/siren_on", payload="ok", repeat=True, ) mock.put( "https://api.ring.com/clients_api/doorbots/987652/siren_off", payload="ok", repeat=True, ) mock.get( "https://api.ring.com/groups/v1/locations/mock-location-id/groups", payload=load_fixture_as_dict("ring_groups.json"), repeat=True, ) mock.get( "https://api.ring.com/groups/v1/locations/" "mock-location-id/groups/mock-group-id/devices", payload=load_fixture_as_dict("ring_group_devices.json"), repeat=True, ) mock.post( "https://api.ring.com/groups/v1/locations/" "mock-location-id/groups/mock-group-id/devices", payload="ok", repeat=True, ) mock.patch( re.compile( r"https:\/\/api\.ring\.com\/devices\/v1\/devices\/\d+\/settings" ), payload="ok", repeat=True, ) mock.get( re.compile(r"https:\/\/api\.ring\.com\/clients_api\/dings\/\d+\/recording"), status=200, body=b"123456", repeat=True, ) mock.get( "https://api.ring.com/clients_api/dings/9876543212/recording", status=200, body=b"123456", repeat=True, ) mock.patch( "https://api.ring.com/clients_api/device", callback=putpatch_status_fixture.callback, repeat=True, ) mock.put( re.compile(r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/.*$"), status=204, body=b"", repeat=True, ) mock.get( "https://api.ring.com/devices/v1/devices/185036587/settings", payload=load_fixture_as_dict("ring_intercom_settings.json"), repeat=True, ) mock.get( "https://api.ring.com/clients_api/locations/mock-location-id/users", payload=load_fixture_as_dict("ring_intercom_users.json"), repeat=True, ) mock.post( "https://api.ring.com/clients_api/locations/mock-location-id/invitations", payload="ok", repeat=True, ) mock.delete( ( "https://api.ring.com/clients_api/locations/" "mock-location-id/invitations/123456789" ), payload="ok", repeat=True, ) requestid = "44529542-3ed7-41da-807e-c170a01bac1d" mock.put( "https://api.ring.com/commands/v1/devices/185036587/device_rpc", body=( '{"result": {"code": 0}, "id": "' + requestid + '", "jsonrpc": "2.0"}' ).encode(), repeat=True, ) yield mock python-ring-doorbell-0.9.13/tests/fixtures/000077500000000000000000000000001472135273200207035ustar00rootroot00000000000000python-ring-doorbell-0.9.13/tests/fixtures/listen/000077500000000000000000000000001472135273200222015ustar00rootroot00000000000000python-ring-doorbell-0.9.13/tests/fixtures/listen/camera_motion_analytics.json000066400000000000000000000004251472135273200277610ustar00rootroot00000000000000{ "server_correlation_id": "1234abcd-12ab-12ab-ab12-1234567abcde", "server_id": "com.ring.pns", "subcategory": "motion", "triggered_at": 1724945621824, "sent_at": 1724945622331, "referring_item_type": "device_id", "referring_item_id": "123456789" } python-ring-doorbell-0.9.13/tests/fixtures/listen/camera_motion_android_config.json000066400000000000000000000002421472135273200307340ustar00rootroot00000000000000{ "category": "com.ring.pn.live-event.motion", "channel": "motion_channel_notification123456789", "body": "There is motion at your Garden Floodcam" } python-ring-doorbell-0.9.13/tests/fixtures/listen/camera_motion_data.json000066400000000000000000000015111472135273200267000ustar00rootroot00000000000000{ "device": { "e2ee_enabled": false, "id": 123456789, "kind": "floodlight_v2", "name": "Garden Floodcam" }, "event": { "ding": { "id": "1234567890123456789", "created_at": "2024-08-29T15:33:41Z", "subtype": "motion", "detection_type": "motion" }, "eventito": { "type": "motion", "timestamp": 1724945621824 }, "riid": "0123456789abcdef0123456789abcdef", "is_sidewalk": false, "live_session": { "streaming_data_hash": "sh-v1|1228_characters_urlsafe_b64", "active_streaming_profile": "rms", "default_audio_route": "loud_speaker", "max_duration": 600 } }, "location": { "id": "zyxw12-4wxyz-0" } } python-ring-doorbell-0.9.13/tests/fixtures/listen/camera_motion_gcmdata.json000066400000000000000000000017131472135273200273730ustar00rootroot00000000000000{ "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.9.13/tests/fixtures/listen/camera_motion_img.json000066400000000000000000000001111472135273200265360ustar00rootroot00000000000000{ "snapshot_uuid": "12345678-ab12-1234-5678-123456abcdef:12345678" } python-ring-doorbell-0.9.13/tests/fixtures/listen/doorbot_ding_gcmdata.json000066400000000000000000000014731472135273200272320ustar00rootroot00000000000000{ "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": "lpd_v1", "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.9.13/tests/fixtures/listen/fcmdata_v1.json000066400000000000000000000002511472135273200250770ustar00rootroot00000000000000{ "data": { "gcmData": "${gcm_data}" }, "from": "123456789012", "priority": "high", "fcmMessageId": "1234fdeb-1234-bc78-cd12-abcdef123456" } python-ring-doorbell-0.9.13/tests/fixtures/listen/fcmdata_v2.json000066400000000000000000000005021472135273200250770ustar00rootroot00000000000000{ "data": { "version": "2.0.0", "data": "${data}", "analytics": "${analytics}", "android_config": "${android_config}", "img": "${img}", "video": {} }, "from": "123456789012", "priority": "normal", "fcmMessageId": "1234fdeb-1234-bc78-cd12-abcdef123456" } python-ring-doorbell-0.9.13/tests/fixtures/listen/intercom_ding_analytics.json000066400000000000000000000004231472135273200277630ustar00rootroot00000000000000{ "server_correlation_id": "1234abcd-12ab-12ab-ab12-1234567abcde", "server_id": "com.ring.pns", "subcategory": "ding", "triggered_at": 1721635228182, "sent_at": 1721635228372, "referring_item_type": "device_id", "referring_item_id": "123456789" } python-ring-doorbell-0.9.13/tests/fixtures/listen/intercom_ding_android_config.json000066400000000000000000000001461472135273200307430ustar00rootroot00000000000000{ "category": "com.ring.pn.live-event.intercom", "body": "🔔 Someone is at your Entrance" } python-ring-doorbell-0.9.13/tests/fixtures/listen/intercom_ding_data.json000066400000000000000000000036131472135273200267110ustar00rootroot00000000000000{ "device": { "e2ee_enabled": false, "id": 298361211, "kind": "intercom_handset_audio", "name": "Entrance" }, "event": { "ding": { "id": "7394367000199864699", "created_at": "2024-07-22T08:00:28.182Z", "subtype": "ding" }, "is_sidewalk": false, "live_session": { "streaming_data_hash": "sh-v1|wVrxK3siWHM-rDBmVyqHvnthjtKk6NzDBahF23xoEPqN9UQ7Oyn_4dg5XgUDGPZsgB1hrMe3e8yU4R-iiQJLArIrb9rKGmhT3OlzVT8WnX8kopIFZKHRm9lHqzsBQ-Xj0t_BIuK5RBAoFLHYOyF4d9zp19MkDu9dGUQWWkMoW-zTbbSCimOCxTeOxo8jsLf3yfOG5UadzYGLBN-91tQ_6zIbU0r5RXdh4z1IXmnpNJSl-61iupCVYdhMWdGtmelMhR2tDO32pk0647I286tyCl0pM2MMsqMl_XdEZxA4vMI2fIA47FZkP7Xvuj21Mbaj9--bMOOT9HXfH3NnpuMTIs_QkKnWCrc0VGn_YUNywuSySC_qv4tCjco7Uto7xFLVYwjAJmKFCMsG3cqz00OSR8j9MbLnZLH9-MDyDUewD28pxGq7bM1nHsUqKN0kTdqeEd_gyLmsAAvnEo-ZQEumXID8hcvZxKFdVVKAFdnk4cD8wrL6t2w5KUXQCW54-olZGTK32e9pY6MbdOHeWTe1VnXM4TSG7P0Sv0MgfUR8ZrsH8-4ov-xaiWL6DBeO0lvwIejKyr56IworUhVzNSZhGOTBPgEB5-T-rxUiGRBedv35x_3xETpRPurEKJ3rRPA06VtD6vyY5-NXcMrEErZHZI_ob60JYT8-BqiY2SxYchqlKTm8zb00dPO6fZXl12U0j6OS7BIIYo2wSGWK8enlDjrTgLFtd0RIqtYIYaZrcxa2GJVaK_u7_UD_l5TnNh1jHrwLXNnSQJV_ife4beNbMOaaWzCuVwmbsOx1Mk84xz9jagMslCYPn1gi3AfKSlewwVTr7TIwSxVjOnVxpBIDydzVIhJliyh95q8Uy7XFm3CrwQbCLXSaEKmARiIFtq773zhuwBv2sH6N1NaVyxtLd1U0FQxipwz8f0SgEaoInUfxeWPRxKNHLkzGR2OK9PLLJN-pWJPurrhjbHZ5tlL-0xuI1IFXTlpU9J2mmobdjy1pcy8EZndZKMTCaR1puRIBsn0hZhErpzBZvdceLZ-Jh5iIO9tQr4mG1pRL_A7wjaTfqhja6oKobjQkdFZEALKzsyp5PsMN7EzRBU9Ckt6V0fZ-b1oJfOyN0ldmtP7Cyqmb5oplr5Ukn07ROmBs78JIH5XetaikmKT4Ib0IzEC_Mf8LpYRx3K-hhYIpTWEhQLwzyQpBsToLhdcC8t352Ge6ON-RwDDpQlQe8tp5rMVSMHryciOfSyVIh3K5GIKJJ5YSBlImoC8exECYcnu-4YW7iRp2", "active_streaming_profile": "rms", "default_audio_route": "quiet_speaker", "max_duration": 600 } }, "location": { "id": "99b3e09a-948d-44b4-bab5-0cc40eee9b36" } } python-ring-doorbell-0.9.13/tests/fixtures/listen/intercom_unlock_gcmdata.json000066400000000000000000000004571472135273200277550ustar00rootroot00000000000000{ "aps": { "alert": "Your Ingresso in Casetta was used to unlock the entrance on 2\\/2\\/24 at 12:10 PM" }, "action": "com.ring.push.INTERCOM_UNLOCK_FROM_APP", "alarm_meta": { "device_zid": 185036587, "location_id": "1234abcd-12cd-123f-de12-0123456789ab" } } python-ring-doorbell-0.9.13/tests/fixtures/ring_chime_health_attrs.json000066400000000000000000000010651472135273200264460ustar00rootroot00000000000000{ "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.9.13/tests/fixtures/ring_devices.json000066400000000000000000000404641472135273200242470ustar00rootroot00000000000000{ "authorized_doorbots": [ { "address": "123 Second St", "alerts": {"connection": "online"}, "battery_life": 51, "description": "Back Door", "device_id": "aacdef124", "external_connection": false, "features": { "advanced_motion_enabled": false, "motion_message_enabled": false, "motions_enabled": true, "people_only_enabled": false, "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", "id": 987653, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, "motion_snooze": null, "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Foo", "id": 999999, "last_name": "Bar"}, "settings": { "chime_settings": { "duration": 3, "enable": true, "type": 1}, "doorbell_volume": 5, "enable_vod": true, "live_view_preset_profile": "highest", "live_view_presets": [ "low", "middle", "high", "highest"], "motion_detection_enabled": false, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": [ "none", "low", "medium", "high"]}, "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York"}], "chimes": [ { "address": "123 Main St", "alerts": {"connection": "online"}, "description": "Downstairs", "device_id": "abcdef123", "do_not_disturb": {"seconds_left": 0}, "features": {"ringtones_enabled": true}, "firmware_version": "1.2.3", "id": 999999, "kind": "chime", "latitude": 12.000000, "longitude": -70.12345, "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Marcelo", "id": 999999, "last_name": "Assistant"}, "settings": { "ding_audio_id": null, "ding_audio_user_id": null, "motion_audio_id": null, "motion_audio_user_id": null, "volume": 2}, "time_zone": "America/New_York"}], "doorbots": [ { "address": "123 Main St", "alerts": {"connection": "online"}, "battery_life": 4081, "description": "Front Door", "device_id": "aacdef123", "external_connection": false, "features": { "advanced_motion_enabled": false, "motion_message_enabled": false, "motions_enabled": true, "people_only_enabled": false, "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", "id": 987652, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, "motion_snooze": null, "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Home", "id": 999999, "last_name": "Assistant"}, "settings": { "chime_settings": { "duration": 3, "enable": true, "type": 0}, "doorbell_volume": 1, "enable_vod": true, "live_view_preset_profile": "highest", "live_view_presets": [ "low", "middle", "high", "highest"], "motion_detection_enabled": true, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": [ "null", "low", "medium", "high"]}, "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York"}], "stickup_cams": [ { "address": "123 Main St", "alerts": {"connection": "online"}, "battery_life": 100, "description": "Front", "device_id": "aacdef123", "external_connection": false, "features": { "advanced_motion_enabled": false, "motion_message_enabled": false, "motions_enabled": true, "night_vision_enabled": false, "people_only_enabled": false, "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", "id": 987652, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "off", "location_id": "mock-location-id", "longitude": -70.12345, "motion_snooze": {"scheduled": true}, "night_mode_status": "false", "owned": true, "owner": { "email": "foo@bar.org", "first_name": "Foo", "id": 999999, "last_name": "Bar"}, "ring_cam_light_installed": "false", "ring_id": null, "settings": { "chime_settings": { "duration": 10, "enable": true, "type": 0}, "doorbell_volume": 11, "enable_vod": true, "floodlight_settings": { "duration": 30, "priority": 0}, "light_schedule_settings": { "end_hour": 0, "end_minute": 0, "start_hour": 0, "start_minute": 0}, "live_view_preset_profile": "highest", "live_view_presets": [ "low", "middle", "high", "highest"], "motion_detection_enabled": false, "motion_announcement": false, "motion_snooze_preset_profile": "low", "motion_snooze_presets": [ "none", "low", "medium", "high"], "motion_zones": { "active_motion_filter": 1, "advanced_object_settings": { "human_detection_confidence": { "day": 0.7, "night": 0.7}, "motion_zone_overlap": { "day": 0.1, "night": 0.2}, "object_size_maximum": { "day": 0.8, "night": 0.8}, "object_size_minimum": { "day": 0.03, "night": 0.05}, "object_time_overlap": { "day": 0.1, "night": 0.6} }, "enable_audio": false, "pir_settings": { "sensitivity1": 1, "sensitivity2": 1, "sensitivity3": 1, "zone_mask": 6}, "sensitivity": 5, "zone1": { "name": "Zone 1", "state": 2, "vertex1": {"x": 0.0, "y": 0.0}, "vertex2": {"x": 0.0, "y": 0.0}, "vertex3": {"x": 0.0, "y": 0.0}, "vertex4": {"x": 0.0, "y": 0.0}, "vertex5": {"x": 0.0, "y": 0.0}, "vertex6": {"x": 0.0, "y": 0.0}, "vertex7": {"x": 0.0, "y": 0.0}, "vertex8": {"x": 0.0, "y": 0.0}}, "zone2": { "name": "Zone 2", "state": 2, "vertex1": {"x": 0.0, "y": 0.0}, "vertex2": {"x": 0.0, "y": 0.0}, "vertex3": {"x": 0.0, "y": 0.0}, "vertex4": {"x": 0.0, "y": 0.0}, "vertex5": {"x": 0.0, "y": 0.0}, "vertex6": {"x": 0.0, "y": 0.0}, "vertex7": {"x": 0.0, "y": 0.0}, "vertex8": {"x": 0.0, "y": 0.0}}, "zone3": { "name": "Zone 3", "state": 2, "vertex1": {"x": 0.0, "y": 0.0}, "vertex2": {"x": 0.0, "y": 0.0}, "vertex3": {"x": 0.0, "y": 0.0}, "vertex4": {"x": 0.0, "y": 0.0}, "vertex5": {"x": 0.0, "y": 0.0}, "vertex6": {"x": 0.0, "y": 0.0}, "vertex7": {"x": 0.0, "y": 0.0}, "vertex8": {"x": 0.0, "y": 0.0}}}, "pir_motion_zones": [0, 1, 1], "pir_settings": { "sensitivity1": 1, "sensitivity2": 1, "sensitivity3": 1, "zone_mask": 6}, "stream_setting": 0, "video_settings": { "ae_level": 0, "birton": null, "brightness": 0, "contrast": 64, "saturation": 80}}, "siren_status": {"seconds_remaining": 0}, "stolen": false, "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York" }], "other": [ { "id": 185036587, "kind": "intercom_handset_audio", "description": "Ingress", "location_id": "mock-location-id", "schema_id": null, "is_sidewalk_gateway": false, "created_at": "2023-12-01T18:05:25Z", "deactivated_at": null, "owner": { "id": 762490876, "first_name": "", "last_name": "", "email": "" }, "device_id": "124ba1b3fe1a", "time_zone": "Europe/Rome", "firmware_version": "Up to Date", "owned": true, "ring_net_id": null, "settings": { "features_confirmed": 5, "show_recordings": true, "recording_ttl": 180, "recording_enabled": false, "keep_alive": null, "keep_alive_auto": 45.0, "doorbell_volume": 8, "enable_chime": 1, "theft_alarm_enable": 0, "use_cached_domain": 1, "use_server_ip": 0, "server_domain": "fw.ring.com", "server_ip": null, "enable_log": 1, "forced_keep_alive": null, "mic_volume": 11, "chime_settings": { "enable": true, "type": 2, "duration": 10 }, "intercom_settings": { "ring_to_open": false, "predecessor": "{\"make\":\"Comelit\",\"model\":\"2738W\",\"wires\":2}", "config": "{\"intercom_type\": 2, \"number_of_wires\": 2, \"autounlock_enabled\": false, \"speaker_gain\": [-49, -35, -25, -21, -16, -9, -6, -3, 0, 3, 6, 9], \"digital\": {\"audio_amp\": 0, \"chg_en\": false, \"fast_chg\": false, \"bypass\": false, \"idle_lvl\": 32, \"ext_audio\": false, \"ext_audio_term\": 0, \"off_hk_tm\": 0, \"unlk_ka\": false, \"unlock\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"ring\": {\"cap_tm\": 40, \"rpl_tm\": 200, \"gain\": 2000, \"cmp_thr\": 4500, \"lvl\": 28000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"m\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_off\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}, \"hook_on\": {\"cap_tm\": 1000, \"rpl_tm\": 1500, \"gain\": 1000, \"cmp_thr\": 4000, \"lvl\": 25000, \"thr\": 30, \"thr2\": 0, \"offln\": false, \"ac\": true, \"bias\": \"h\", \"tx_pin\": \"TXD2\", \"ack\": 0, \"prot\": \"Comelit_SB2\", \"ask\": {\"f\": 25000, \"b\": 333}}}}", "intercom_type": "DF", "replication": 1, "unlock_mode": 0 }, "voice_volume": 11 }, "alerts": { "connection": "online", "ota_status": "timeout" }, "function": { "name": null }, "subscribed": false, "battery_life": "52", "features": { "cfes_eligible": false, "motion_zone_recommendation": false, "motions_enabled": true, "show_recordings": true, "show_vod_settings": true, "rich_notifications_eligible": false, "show_offline_motion_events": false, "sheila_camera_eligible": null, "sheila_camera_processing_eligible": null, "dynamic_network_switching_eligible": false, "chime_auto_detect_capable": false, "missing_key_delivery_address": false, "show_24x7_lite": false, "recording_24x7_eligible": null }, "metadata": { "ethernet": false, "legacy_fw_migrated": true, "imported_from_amazon": false, "is_sidewalk_gateway": false, "key_access_point_associated": true } }, { "id": 99999999, "kind": "three_p_cam", "description": "Third party Cam", "location_id": "**REDACTED**", "schema_id": null, "is_sidewalk_gateway": true, "deactivated_at": null, "hardware_id": "ABCD12345", "time_zone": "America/Phoenix", "stolen": false, "owned": true, "settings": { "enable_vod": 0, "powered_on": null, "supported_capabilities": {}, "camera_stream_configurations": [] }, "features": { "cfes_eligible": false, "motion_zone_recommendation": false, "motions_enabled": true, "show_recordings": false, "show_vod_settings": true, "show_offline_motion_events": false }, "owner": { "id": "**REDACTED**", "first_name": "", "last_name": "", "email": "" }, "ring_net_id": "**REDACTED**", "third_party_manufacturer": "amazon1p", "third_party_model": "A3RMGO6LYLH7YN", "third_party_dsn": "ABCD12345", "third_party_tags": [], "metadata": { "third_party_manufacturer": "amazon1p", "third_party_model": "A3RMGO6LYLH7YN", "is_sidewalk_gateway": true, "third_party_dsn": "ABCD12345" }, "alerts": { "connection": "online" } } ] } python-ring-doorbell-0.9.13/tests/fixtures/ring_devices_updated.json000066400000000000000000000243441472135273200257540ustar00rootroot00000000000000{ "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.9.13/tests/fixtures/ring_ding_active.json000066400000000000000000000046221472135273200250750ustar00rootroot00000000000000[ { "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.9.13/tests/fixtures/ring_doorboot_health_attrs.json000066400000000000000000000010651472135273200272100ustar00rootroot00000000000000{ "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.9.13/tests/fixtures/ring_doorboot_health_attrs_id987653.json000066400000000000000000000010651472135273200303720ustar00rootroot00000000000000{ "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.9.13/tests/fixtures/ring_doorbot_history.json000066400000000000000000000012301472135273200260420ustar00rootroot00000000000000[{ "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.9.13/tests/fixtures/ring_group_devices.json000066400000000000000000000013471472135273200254600ustar00rootroot00000000000000{ "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.9.13/tests/fixtures/ring_groups.json000066400000000000000000000014221472135273200241330ustar00rootroot00000000000000{ "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.9.13/tests/fixtures/ring_intercom_history.json000066400000000000000000000051471472135273200262250ustar00rootroot00000000000000[ { "id": 7330963245622279024, "created_at": "2024-02-02T11:21:24.000Z", "answered": false, "events": [], "kind": "ding", "favorite": false, "snapshot_url": "", "recording": { "status": "ready" }, "duration": 40.0, "cv_properties": { "person_detected": null, "stream_broken": false, "detection_type": null, "cv_triggers": null, "detection_types": null, "security_alerts": null }, "properties": { "is_alexa": false, "is_sidewalk": false, "is_autoreply": false }, "doorbot": { "id": 185036587, "description": "Ingresso", "type": "intercom_handset_audio" }, "device_placement": null, "geolocation": null, "last_location": null, "siren": null, "is_e2ee": false, "had_subscription": false, "owner_id": "762490876" }, { "id": 7323267080901445808, "created_at": "2024-01-12T17:36:28.000Z", "answered": true, "events": [], "kind": "on_demand", "favorite": false, "snapshot_url": "", "recording": { "status": "ready" }, "duration": 13.0, "cv_properties": { "person_detected": null, "stream_broken": false, "detection_type": null, "cv_triggers": null, "detection_types": null, "security_alerts": null }, "properties": { "is_alexa": false, "is_sidewalk": false, "is_autoreply": false }, "doorbot": { "id": 185036587, "description": "Ingresso", "type": "intercom_handset_audio" }, "device_placement": null, "geolocation": null, "last_location": null, "siren": null, "is_e2ee": false, "had_subscription": false, "owner_id": "762490876" }, { "id": 7307399027047288688, "created_at": "2023-12-01T18:44:28.000Z", "answered": true, "events": [], "kind": "on_demand", "favorite": false, "snapshot_url": "", "recording": { "status": "ready" }, "duration": 43.0, "cv_properties": { "person_detected": null, "stream_broken": false, "detection_type": null, "cv_triggers": null, "detection_types": null, "security_alerts": null }, "properties": { "is_alexa": false, "is_sidewalk": false, "is_autoreply": false }, "doorbot": { "id": 185036587, "description": "Ingresso", "type": "intercom_handset_audio" }, "device_placement": null, "geolocation": null, "last_location": null, "siren": null, "is_e2ee": false, "had_subscription": false, "owner_id": "762490876" } ] python-ring-doorbell-0.9.13/tests/fixtures/ring_intercom_settings.json000066400000000000000000000245521472135273200263650ustar00rootroot00000000000000{ "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.9.13/tests/fixtures/ring_intercom_users.json000066400000000000000000000012251472135273200256560ustar00rootroot00000000000000[ { "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.9.13/tests/fixtures/ring_listen_credentials.json000066400000000000000000000044171472135273200264760ustar00rootroot00000000000000{ "gcm": { "token": "XYZ01234zyx:APA91b0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ-01234abcdefghijklmnopqrstuvwxyz", "app_id": "abcdef01-ef12-1234-12ef-abcdef012345", "android_id": "5678901234567890123", "security_token": "0123456789012345678" }, "keys": { "public": "BPEHm32RWI4db8FCk0IM6G9f9vz_uJeRiuU64Y5dkyZjkBXmyGgzwzZMylPaLNvg50EuoQmNlU7sSMUf0mYctn0=", "private": "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg-jTz69Lsq4oa49_5PMMkcqYFwkEGc5L-zGDwUwfY5eahRANCAATxB5t9kViOHW_BQpNCDOhvX_b8_7iXkYrlOuGOXZMmY5AV5shoM8M2TMpT2izb4OdBLqEJjZVO7EjFH9JmHLZ9", "secret": "_zGHqwp9rRP5cgzilMLvCA" }, "fcm": { "registration": { "name": "projects/project-1234/registrations/abcdefghijklmnopqrstuvwxyz-0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ-01234abcdefghijklmnopqrstuvwxyz_XYZ01234zyx:APA91b0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ", "token": "abcdefghijklmnopqrstuvwxyz-0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ-01234abcdefghijklmnopqrstuvwxyz_XYZ01234zyx:APA91b0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ", "web": { "endpoint": "https://fcm.googleapis.com/fcm/send/XYZ01234zyx:APA91b0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZ-01234abcdefghijklmnopqrstuvwxyz", "p256dh": "123456967219824KDJDOWFNAFW=", "auth": "DSFNDSAKGFAGFA==" } }, "installation": { "token": "OPQRSTUVWXYZ-01234abcdefghijklOPQRSTUVWXYZ-01234abcdefghijklOPQRSTUVWXYZ-01234abcdefghijklOPQRSTUVWXYZ-01234abcdefghijklOPQRSTUVWXYZ-01234abcdefghijklOPQRSTUVWXYZ-01234abcdefghijklOPQRSTUVWXYZ-01234abcdefghijklOPQRSTUVWXYZ-01234abcdefghijkl", "expires_in": 604800, "refresh_token": "1_OPQRSTUVWXYZ-01234abcdefghijklOPQRSTUVWXYZ-01234abcdefghijklOPQRSTUVWXYZ-01234abcdefghijklOPQRSTUVWXYZ-01234abcdefghijkl", "fid": "1234AMXDRTTYODLsd-nb14", "created_at": 36245.436300371 } }, "config": { "bundle_id": "project.push.com", "project_id": "project-1234", "vapid_key": "BDOU99-h67HcA6JeFXHbSNMu7e2yNNu3RzoMj8TM4W88jITfq7ZmPvIM1Iv-4_l2LxQcYwhqby2xGpWwzjfAnG4" } } python-ring-doorbell-0.9.13/tests/fixtures/ring_oauth.json000066400000000000000000000002711472135273200237350ustar00rootroot00000000000000{ "access_token": "dummyBearerToken", "token_type": "bearer", "expires_in": 3600, "refresh_token": "123123456789", "scope": "client", "created_at": 1529099870 } python-ring-doorbell-0.9.13/tests/fixtures/ring_session.json000066400000000000000000000023401472135273200242770ustar00rootroot00000000000000{ "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.9.13/tests/ruff.toml000066400000000000000000000012211472135273200206650ustar00rootroot00000000000000# This extends our general Ruff rules specifically for tests extend = "../pyproject.toml" lint.extend-select = [ "PT", # Use @pytest.fixture without parentheses ] lint.extend-ignore = [ "S101", # Use of assert detected. As these are tests... "S105", # Detection of passwords... "S106", # Detection of passwords... "SLF001", # Tests will access private/protected members... "TCH002", # pytest doesn't like this one... "PLR0913", # we're overwriting function that has many arguments "ANN001", # type annotations - TODO "ANN201", # return type annotations - TODO "D103", # docstring - TODO "ARG001", # Unused function argument - TODO ] python-ring-doorbell-0.9.13/tests/test_cli.py000066400000000000000000000302401472135273200212110ustar00rootroot00000000000000"""Module for cli tests.""" from __future__ import annotations import json from pathlib import Path from typing import Any from unittest.mock import DEFAULT, MagicMock, call, patch import aiofiles import pytest from asyncclick.testing import CliRunner from ring_doorbell import AuthenticationError, Requires2FAError, Ring from ring_doorbell.cli import ( _event_handler, cli, devices_command, in_home_chime, list_command, listen, motion_detection, open_door, show, videos, ) from ring_doorbell.const import GCM_TOKEN_FILE from tests.conftest import ( load_alert_v1, load_fixture, load_fixture_as_dict, ) async def test_cli_default(ring): runner = CliRunner() with runner.isolated_filesystem(): res = await runner.invoke( cli, ["--username", "foo", "--password", "foo"], obj=ring ) expected = ( "Name: Downstairs\nFamily: chimes\nID:" " 999999\nTimezone: America/New_York\nWifi Name:" " ring_mock_wifi\nWifi RSSI: -39\n\n" ) assert res.exit_code == 0 assert expected in res.output async def test_show(ring): runner = CliRunner() with runner.isolated_filesystem(): res = await runner.invoke(show, obj=ring) expected = ( "Name: Downstairs\nFamily: chimes\nID:" " 999999\nTimezone: America/New_York\nWifi Name:" " ring_mock_wifi\nWifi RSSI: -39\n\n" ) assert res.exit_code == 0 assert expected in res.output async def test_devices(ring): runner = CliRunner() with runner.isolated_filesystem(): res = await runner.invoke(devices_command, obj=ring) expected = ( "(Pretty format coming soon, if you want json " "consistently from this command provide the --json flag)\n" ) for device_type in ring.devices_data: for device_api_id in ring.devices_data[device_type]: expected += ( json.dumps(ring.devices_data[device_type][device_api_id], indent=2) + "\n" ) assert res.exit_code == 0 assert expected == res.output async def test_list(ring): runner = CliRunner() with runner.isolated_filesystem(): res = await runner.invoke(list_command, obj=ring) expected = ( "Front Door (lpd_v1)\nBack Door (lpd_v1)\nDownstairs (chime)\n" "Front (hp_cam_v1)\nIngress (intercom_handset_audio)\n" ) assert res.exit_code == 0 assert expected in res.output aiofiles.threadpool.wrap.register(MagicMock)( lambda *args, **kwargs: aiofiles.threadpool.AsyncBufferedIOBase(*args, **kwargs) ) async def test_videos(ring, mocker): runner = CliRunner() mock_file_stream = MagicMock(read=lambda *args, **kwargs: b"") # noqa: ARG005 with runner.isolated_filesystem(): with patch( "aiofiles.threadpool.sync_open", return_value=mock_file_stream ) as mock_open: res = await runner.invoke(videos, ["--count", "--download-all"], obj=ring) assert mock_open.call_count == 3 mock_file_stream.write.assert_has_calls([call(b"123456")] * 3) assert "Downloading 3 videos" in res.output @pytest.mark.parametrize( ("affect_method", "exception", "file_exists"), [ (None, None, False), ("ring_doorbell.auth.Auth.async_fetch_token", Requires2FAError, False), ("ring_doorbell.ring.Ring.async_update_data", AuthenticationError, True), ], ids=("No 2FA", "Require 2FA", "Invalid Grant"), ) async def test_auth(mocker, affect_method, exception, file_exists): call_count = 0 def _raise_once(self, *_: dict[str, Any], **__: dict[str, Any]) -> dict[str, Any]: nonlocal call_count, exception if call_count == 0: call_count += 1 msg = "Simulated exception" raise exception(msg) call_count += 1 if hasattr(self, "_update_data"): return self._update_data() return DEFAULT def _add_token( uri, http_method, body, headers, token_placement=None, **kwargs, # noqa: ANN003 ) -> tuple[str, dict, bytes]: return uri, headers, body mocker.patch( "oauthlib.oauth2.LegacyApplicationClient.add_token", side_effect=_add_token ) 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, aioresponses_mock, devices_fixture): 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. devices_fixture.updated = True aioresponses_mock.get( "https://api.ring.com/clients_api/ring_devices", payload=load_fixture_as_dict("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.nolistenmock async def test_listen_store_credentials(mocker, auth): runner = CliRunner() import firebase_messaging credentials = json.loads(load_fixture("ring_listen_credentials.json")) with runner.isolated_filesystem(): mocker.patch( "firebase_messaging.fcmregister.FcmRegister.gcm_check_in", return_value="foobar", ) mocker.patch( "firebase_messaging.fcmregister.FcmRegister.register", return_value=credentials, ) mocker.patch("firebase_messaging.FcmPushClient.start") mocker.patch("firebase_messaging.FcmPushClient.is_started", return_value=True) ring = Ring(auth) assert not Path(GCM_TOKEN_FILE).is_file() await runner.invoke( listen, ["--store-credentials"], obj=ring, catch_exceptions=True ) assert Path(GCM_TOKEN_FILE).is_file() assert firebase_messaging.fcmregister.FcmRegister.gcm_check_in.call_count == 0 assert firebase_messaging.fcmregister.FcmRegister.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.fcmregister.FcmRegister.gcm_check_in.call_count == 1 assert firebase_messaging.fcmregister.FcmRegister.register.call_count == 1 assert firebase_messaging.FcmPushClient.start.call_count == 2 async def test_listen_event_handler(mocker, auth): from ring_doorbell.listen import RingEventListener ring = Ring(auth) listener = RingEventListener(ring) await listener.start() listener.add_notification_callback(_event_handler(ring).on_event) msg = load_alert_v1( "camera_motion", 12345678, created_at="2023-10-24T09:42:18.789709Z" ) 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=1698140538.789709," " expires_in=180, kind='motion', state='human', is_update=False) : " "Currently active count = 1" ) echomock.assert_called_with(exp) async def test_in_home_chime(ring, aioresponses_mock, devices_fixture): runner = CliRunner() with runner.isolated_filesystem(): # Gets in-home chime details for a doorbell res = await runner.invoke( in_home_chime, ["--device-name", "Front Door"], obj=ring, ) expected_show = ( "Name: Front Door\n" "ID: 987652\n" "Type: Mechanical\n" "Enabled: True\n" "Duration: None\n" ) expected = expected_show assert res.exit_code == 0 assert expected in res.output # Turns off the in-home chime res = await runner.invoke( in_home_chime, ["--device-name", "Front Door", "enabled", "False"], obj=ring, ) expected = "Front Door's in-home chime has been disabled" assert res.exit_code == 0 assert expected in res.output # Turns on the in-home chime and sets type to Mechanical res = await runner.invoke( in_home_chime, [ "--device-name", "Front Door", "type", "mechanical", ], obj=ring, ) expected = "Front Door's in-home chime type has been set to Mechanical\n" assert res.exit_code == 0 assert expected in res.output # Sets type to Digital and changes the duration res = await runner.invoke( in_home_chime, ["--device-name", "Front Door", "duration", "5"], obj=ring, ) expected = "Front Door's in-home chime duration has been set to 5 seconds\n" assert res.exit_code == 0 assert expected in res.output # Runs in-home-chime against a device that doesn't have an in-home chime res = await runner.invoke( in_home_chime, ["--device-name", "Front"], obj=ring, ) expected = "Front is not a doorbell" assert res.exit_code == 1 assert expected in res.output # Runs in-home-chime with no parameters and error as more than one doorbot. res = await runner.invoke( in_home_chime, [], obj=ring, ) expected = "There are 2 doorbells, you need to pass the --device-name option" assert res.exit_code == 1 assert expected in res.output async def test_open_door(ring, aioresponses_mock, devices_fixture): runner = CliRunner() res = await runner.invoke( open_door, ["--device-name", "Ingress"], obj=ring, ) assert res.exit_code == 0 assert res.output == "Ingress opened\n" async def test_get_device(ring, aioresponses_mock, devices_fixture): runner = CliRunner() # Get device by name res = await runner.invoke( open_door, ["--device-name", "Ingress"], obj=ring, ) assert res.exit_code == 0 assert res.output == "Ingress opened\n" # Get device by single type res = await runner.invoke( open_door, [], obj=ring, ) assert res.exit_code == 0 assert res.output == "Ingress opened\n" # Get wrong device type res = await runner.invoke( open_door, ["--device-name", "Front"], obj=ring, ) assert res.exit_code == 1 assert "Front is not a intercom" in res.output # Wrong name res = await runner.invoke( open_door, ["--device-name", "Frontx"], obj=ring, ) assert res.exit_code == 1 assert "Cannot find intercom with name Frontx" in res.output python-ring-doorbell-0.9.13/tests/test_listen.py000066400000000000000000000153161472135273200217470ustar00rootroot00000000000000"""The tests for the Ring platform.""" import datetime import json import pytest from freezegun.api import FrozenDateTimeFactory from ring_doorbell import Ring from ring_doorbell.exceptions import RingError from ring_doorbell.listen import RingEventListener from tests.conftest import load_alert_v1, load_alert_v2, load_fixture async def test_listen(auth, mocker): import firebase_messaging ring = Ring(auth) listener = RingEventListener(ring) await listener.start() assert firebase_messaging.FcmPushClient.checkin_or_register.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) async def test_active_dings(auth, mocker): import firebase_messaging ring = Ring(auth) listener = RingEventListener(ring) await listener.start() assert firebase_messaging.FcmPushClient.checkin_or_register.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 == 0 alertstoadd = 2 for i in range(alertstoadd): msg = load_alert_v1("doorbot_ding", 123456781, ding_id_inc=i) listener._on_notification(msg, "1234567" + str(i)) msg = load_alert_v2("camera_motion", 123456782, ding_id_inc=i) listener._on_notification(msg, "1234567" + str(i)) msg = load_alert_v1("intercom_unlock", 185036587, ding_id_inc=i) listener._on_notification(msg, "1234567" + str(i)) dings = ring.active_alerts() assert len(dings) == num_active + alertstoadd * 3 # Test with the same id which should overwrite # previous and keep the overall count the same for i in range(alertstoadd): msg = load_alert_v1("doorbot_ding", 123456781, ding_id_inc=i) listener._on_notification(msg, "1234567" + str(i)) msg = load_alert_v2("camera_motion", 123456782, ding_id_inc=i) listener._on_notification(msg, "1234567" + str(i)) dings = ring.active_alerts() assert len(dings) == num_active + alertstoadd * 3 await listener.stop() async def test_ding_expirey(auth, mocker, freezer: FrozenDateTimeFactory): ring = Ring(auth) listener = RingEventListener(ring) await listener.start() assert listener.subscribed is True assert listener.started is True assert len(ring.push_dings_data) == 0 assert len(ring.active_alerts()) == 0 alertstoadd = 2 for i in range(alertstoadd): msg = load_alert_v1("doorbot_ding", 123456781, ding_id_inc=i) listener._on_notification(msg, "1234567" + str(i)) msg = load_alert_v2("camera_motion", 123456782, ding_id_inc=i) listener._on_notification(msg, "1234567" + str(i)) msg = load_alert_v1("intercom_unlock", 185036587, ding_id_inc=i) listener._on_notification(msg, "1234567" + str(i)) assert len(ring.push_dings_data) == 6 assert len(ring.active_alerts()) == 6 freezer.tick(datetime.timedelta(minutes=5)) msg = load_alert_v1("doorbot_ding", 123456781, ding_id_inc=alertstoadd + 1) listener._on_notification(msg, "123456781" + str(alertstoadd + 1)) assert len(ring.push_dings_data) == 1 assert len(ring.active_alerts()) == 1 @pytest.mark.nolistenmock async def test_listen_subscribe_fail(auth, mocker, caplog, putpatch_status_fixture): checkinmock = mocker.patch( "firebase_messaging.FcmPushClient.checkin_or_register", return_value="foobar" ) connectmock = mocker.patch("firebase_messaging.FcmPushClient.start") mocker.patch("firebase_messaging.FcmPushClient.is_started", return_value=True) putpatch_status_fixture.overrides["https://api.ring.com/clients_api/device"] = 401 ring = Ring(auth) listener = RingEventListener(ring) await 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 , 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): # 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.fcmregister.FcmRegister.gcm_check_in", return_value=None ) registermock = mocker.patch( "firebase_messaging.fcmregister.FcmRegister.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) await 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, caplog): checkinmock = mocker.patch( "firebase_messaging.FcmPushClient.checkin_or_register", 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) await 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 = "Ring listener 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 ) python-ring-doorbell-0.9.13/tests/test_other.py000066400000000000000000000107211472135273200215650ustar00rootroot00000000000000"""The tests for the Ring platform.""" from .conftest import json_request_kwargs, nojson_request_kwargs async def test_other_attributes(ring): """Test the Ring Other class and methods.""" dev = ring.devices()["other"][0] assert dev.id != 99999 assert dev.device_id == "124ba1b3fe1a" assert dev.kind == "intercom_handset_audio" assert dev.model == "Intercom" assert dev.location_id == "mock-location-id" assert dev.has_capability("battery") is False assert dev.has_capability("open") is True assert dev.has_capability("history") is True assert dev.timezone == "Europe/Rome" assert dev.battery_life == 52 assert dev.doorbell_volume == 8 assert dev.mic_volume == 11 assert await dev.async_get_clip_length_max() == 60 assert dev.connection_status == "online" assert len(await dev.async_get_allowed_users()) == 2 assert dev.subscribed is True assert dev.has_subscription is True assert dev.unlock_duration is None assert dev.keep_alive_auto == 45.0 assert isinstance(await dev.async_history(limit=1, kind="on_demand"), list) assert len(await dev.async_history(kind="ding")) == 1 assert len(await dev.async_history(limit=1, kind="on_demand")) == 2 assert ( len( await dev.async_history( limit=1, kind="on_demand", enforce_limit=True, retry=50 ) ) == 1 ) await dev.async_update_health_data() assert dev.wifi_name == "ring_mock_wifi" assert dev.wifi_signal_category == "good" assert dev.wifi_signal_strength != 100 async def test_other_controls(ring, aioresponses_mock): dev = ring.devices()["other"][0] kwargs = json_request_kwargs() kwargs["json"] = None await dev.async_set_doorbell_volume(6) kwargs["params"] = {"doorbot[settings][doorbell_volume]": "6"} aioresponses_mock.assert_called_with( "https://api.ring.com/clients_api/doorbots/185036587", method="PUT", **kwargs ) kwargs = json_request_kwargs() await dev.async_set_mic_volume(10) kwargs["json"] = {"volume_settings": {"mic_volume": 10}} aioresponses_mock.assert_called_with( "https://api.ring.com/devices/v1/devices/185036587/settings", method="PATCH", **kwargs, ) await dev.async_set_voice_volume(9) kwargs["json"] = {"volume_settings": {"voice_volume": 9}} aioresponses_mock.assert_called_with( "https://api.ring.com/devices/v1/devices/185036587/settings", method="PATCH", **kwargs, ) await dev.async_set_clip_length_max(30) kwargs["json"] = {"video_settings": {"clip_length_max": 30}} aioresponses_mock.assert_called_with( "https://api.ring.com/devices/v1/devices/185036587/settings", method="PATCH", **kwargs, ) await dev.async_set_keep_alive_auto(32.2) kwargs["json"] = {"keep_alive_settings": {"keep_alive_auto": 32.2}} aioresponses_mock.assert_called_with( "https://api.ring.com/devices/v1/devices/185036587/settings", method="PATCH", **kwargs, ) async def test_other_invitations(ring, aioresponses_mock): dev = ring.devices()["other"][0] kwargs = json_request_kwargs() kwargs["json"] = { "invitation": { "doorbot_ids": [185036587], "invited_email": "test@example.com", "group_ids": [], } } await dev.async_invite_access("test@example.com") aioresponses_mock.assert_called_with( "https://api.ring.com/clients_api/locations/mock-location-id/invitations", method="POST", **kwargs, ) await dev.async_remove_access(123456789) kwargs = nojson_request_kwargs() aioresponses_mock.assert_called_with( "https://api.ring.com/clients_api/locations/mock-location-id/invitations/123456789", method="DELETE", **kwargs, ) async def test_other_open_door(ring, aioresponses_mock, mocker): dev = ring.devices()["other"][0] mocker.patch("uuid.uuid4", return_value="987654321") kwargs = json_request_kwargs() kwargs["json"] = { "command_name": "device_rpc", "request": { "id": "987654321", "jsonrpc": "2.0", "method": "unlock_door", "params": {"door_id": 0, "user_id": 15}, }, } await dev.async_open_door(15) aioresponses_mock.assert_called_with( "https://api.ring.com/commands/v1/devices/185036587/device_rpc", method="PUT", **kwargs, ) python-ring-doorbell-0.9.13/tests/test_ring.py000066400000000000000000000270441472135273200214110ustar00rootroot00000000000000"""The tests for the Ring platform.""" import asyncio from datetime import datetime, timezone import pytest from freezegun.api import FrozenDateTimeFactory from ring_doorbell import Auth, Ring, RingError from ring_doorbell.const import MSG_EXISTING_TYPE, USER_AGENT from ring_doorbell.util import parse_datetime from .conftest import json_request_kwargs, load_fixture_as_dict 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 async def test_chime_attributes(ring): """Test the Ring Chime class and methods.""" dev = ring.devices()["chimes"][0] assert dev.address == "123 Main St" assert dev.id != 99999 assert dev.device_id == "abcdef123" assert dev.kind == "chime" assert dev.model == "Chime" assert dev.has_capability("battery") is False assert dev.has_capability("volume") is True assert dev.has_capability("history") is False assert dev.latitude is not None assert dev.timezone == "America/New_York" assert dev.volume == 2 assert len(await dev.async_history()) == 0 await dev.async_update_health_data() assert dev.wifi_name == "ring_mock_wifi" assert dev.wifi_signal_category == "good" assert dev.wifi_signal_strength != 100 async 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(await dev.async_history(limit=1, kind="motion"), list) assert len(await dev.async_history(kind="ding")) == 1 assert len(await dev.async_history(limit=1, kind="motion")) == 2 assert ( len( await dev.async_history( limit=1, kind="motion", enforce_limit=True, retry=50 ) ) == 1 ) assert dev.existing_doorbell_type == "Mechanical" await dev.async_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 async def test_stickup_cam_controls(ring, aioresponses_mock): dev = ring.devices()["stickup_cams"][0] kwargs = json_request_kwargs() kwargs["json"] = None await dev.async_set_lights("off") aioresponses_mock.assert_called_with( url="https://api.ring.com/clients_api/doorbots/987652/floodlight_light_off", method="PUT", **kwargs, ) await dev.async_set_lights("on") aioresponses_mock.assert_called_with( url="https://api.ring.com/clients_api/doorbots/987652/floodlight_light_on", method="PUT", **kwargs, ) await dev.async_set_siren(0) aioresponses_mock.assert_called_with( url="https://api.ring.com/clients_api/doorbots/987652/siren_off", method="PUT", **kwargs, ) await dev.async_set_siren(30) kwargs["params"] = {"duration": 30} aioresponses_mock.assert_called_with( url="https://api.ring.com/clients_api/doorbots/987652/siren_on", method="PUT", **kwargs, ) async def test_light_groups(ring): group = ring.groups()["mock-group-id"] assert group.name == "Landscape" assert group.family == "group" assert group.group_id == "mock-group-id" assert group.location_id == "mock-location-id" assert group.model == "Light Group" assert group.has_capability("light") is True with pytest.raises(RingError): group.has_capability("something-else") with pytest.raises( RingError, match=( "You need to call update on the group before " "accessing the lights property." ), ): assert group.lights is False await group.async_update() # Attempt turning on lights await group.async_set_lights(state=True) # Attempt turning off lights await group.async_set_lights(state=False) # Attempt turning on lights for 30 seconds await group.async_set_lights(state=True, duration=30) async def test_motion_detection_enable(ring, aioresponses_mock): dev = ring.devices()["doorbots"][0] kwargs = json_request_kwargs() await dev.async_set_motion_detection(state=True) kwargs["json"] = {"motion_settings": {"motion_detection_enabled": True}} aioresponses_mock.assert_called_with( url="https://api.ring.com/devices/v1/devices/987652/settings", method="PATCH", **kwargs, ) await dev.async_set_motion_detection(state=False) kwargs["json"] = {"motion_settings": {"motion_detection_enabled": False}} aioresponses_mock.assert_called_with( url="https://api.ring.com/devices/v1/devices/987652/settings", method="PATCH", **kwargs, ) @pytest.mark.parametrize( ("datetime_string", "expected", "error_in_log"), [ pytest.param( "2012-01-15T06:01:01", datetime(2012, 1, 14, 5, 5, 5, 123 * 1_000, tzinfo=timezone.utc), True, id="No timezone", ), pytest.param( "2012-01-15T06:01:01.12Z", datetime(2012, 1, 15, 6, 1, 1, 120 * 1_000, tzinfo=timezone.utc), False, id="Millis", ), pytest.param( "2012-01-15T06:01:01.123456Z", datetime(2012, 1, 15, 6, 1, 1, 123456, tzinfo=timezone.utc), False, id="Micros", ), pytest.param( "2012-01-15T06:01:01Z", datetime(2012, 1, 15, 6, 1, 1, 0, tzinfo=timezone.utc), False, id="No millis", ), ], ) def test_datetime_parse( freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, datetime_string, expected, error_in_log, ): """Test the datetime parsing.""" freezer.move_to("2012-01-14T05:05:05.123Z") dt = parse_datetime(datetime_string) is_error_in_log = ( f"Unable to parse datetime string {datetime_string}, defaulting to now time" in caplog.text ) assert dt == expected assert is_error_in_log is error_in_log async def test_sync_queries_from_event_loop(): auth = Auth(USER_AGENT, token=load_fixture_as_dict("ring_oauth.json")) ring = Ring(auth) assert asyncio.get_running_loop() msg = ( "You cannot call deprecated sync function Ring.update_devices " "from within a running event loop." ) with pytest.raises(RingError, match=msg): ring.update_devices() async def test_sync_queries_from_executor(): auth = Auth(USER_AGENT, token=load_fixture_as_dict("ring_oauth.json")) ring = Ring(auth) loop = asyncio.get_running_loop() assert ring.devices_data == {} # This will run the query inan executor thread msg = "Ring.update_devices is deprecated, use Ring.async_update_devices" with pytest.deprecated_call(match=msg): await loop.run_in_executor(None, ring.update_devices) assert ring.devices_data def test_sync_queries_with_no_event_loop(): auth = Auth(USER_AGENT, token=load_fixture_as_dict("ring_oauth.json")) ring = Ring(auth) assert not ring.devices_data msg = "Ring.update_devices is deprecated, use Ring.async_update_devices" with pytest.deprecated_call(match=msg): ring.update_devices() assert ring.devices_data with pytest.deprecated_call(): auth.close() async def test_set_existing_doorbell_type(ring, aioresponses_mock): data = ring.devices() dev = data["doorbots"][0] assert dev.existing_doorbell_type == "Mechanical" kwargs = json_request_kwargs() kwargs["json"] = None aioresponses_mock.requests.clear() # Attempting to turn off the in-home chime await dev.async_set_existing_doorbell_type_enabled(value=False) kwargs["params"] = { "doorbot[description]": dev.name, "doorbot[settings][chime_settings][enable]": 0, } aioresponses_mock.assert_called_with( url="https://api.ring.com/clients_api/doorbots/987652", method="PUT", **kwargs, ) aioresponses_mock.requests.clear() # Attempting to turn on the in-home chime await dev.async_set_existing_doorbell_type_enabled(value=True) kwargs["params"] = { "doorbot[description]": dev.name, "doorbot[settings][chime_settings][enable]": 1, } aioresponses_mock.assert_called_with( url="https://api.ring.com/clients_api/doorbots/987652", method="PUT", **kwargs, ) aioresponses_mock.requests.clear() # Attempting to set the doorbell type await dev.async_set_existing_doorbell_type(2) kwargs["params"] = { "doorbot[description]": dev.name, "doorbot[settings][chime_settings][type]": 2, } aioresponses_mock.assert_called_with( url="https://api.ring.com/clients_api/doorbots/987652", method="PUT", **kwargs, ) aioresponses_mock.requests.clear() # Attempting to set the duration of the in-home chime settings = dev._attrs["settings"]["chime_settings"] settings["type"] = 1 assert dev.existing_doorbell_type == "Digital" await dev.async_set_existing_doorbell_type_duration(5) kwargs["params"] = { "doorbot[description]": dev.name, "doorbot[settings][chime_settings][duration]": 5, } aioresponses_mock.assert_called_with( url="https://api.ring.com/clients_api/doorbots/987652", method="PUT", **kwargs, ) # Attempting to enable when no chime present settings = dev._attrs["settings"]["chime_settings"] settings["type"] = 2 assert dev.existing_doorbell_type == "Not Present" with pytest.raises(RingError, match="In-Home chime is not present."): await dev.async_set_existing_doorbell_type_enabled(value=True) # Attempting to set the doorbell type to an invalid value with pytest.raises(RingError, match=f"value must be in {MSG_EXISTING_TYPE}"): await dev.async_set_existing_doorbell_type(4) # Attempting to set the doorbell duration to an invalid value with pytest.raises(RingError, match=f"Must be within the {0}-{1}."): await dev.async_set_existing_doorbell_type_duration(11) python-ring-doorbell-0.9.13/uv.lock000066400000000000000000010647511472135273200172120ustar00rootroot00000000000000version = 1 requires-python = ">=3.9.0" [[package]] name = "aiofiles" version = "24.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } wheels = [ { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, ] [[package]] name = "aiohappyeyeballs" version = "2.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bc/69/2f6d5a019bd02e920a3417689a89887b39ad1e350b562f9955693d900c40/aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", size = 21809 } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/d8/120cd0fe3e8530df0539e71ba9683eade12cae103dd7543e50d15f737917/aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572", size = 14742 }, ] [[package]] name = "aiohttp" version = "3.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, { name = "async-timeout", marker = "python_full_version < '3.11'" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, { name = "yarl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4b/cb/f9bb10e0cf6f01730b27d370b10cc15822bea4395acd687abc8cc5fed3ed/aiohttp-3.11.7.tar.gz", hash = "sha256:01a8aca4af3da85cea5c90141d23f4b0eee3cbecfd33b029a45a80f28c66c668", size = 7666482 } wheels = [ { url = "https://files.pythonhosted.org/packages/83/7e/fb4723d280b4de2642c57593cb94f942bfdc15def510d12b5d22a1b955a6/aiohttp-3.11.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8bedb1f6cb919af3b6353921c71281b1491f948ca64408871465d889b4ee1b66", size = 706857 }, { url = "https://files.pythonhosted.org/packages/57/f1/4eb447ad029801b1007ff23025c2bcb2519af2e03085717efa333f1803a5/aiohttp-3.11.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f5022504adab881e2d801a88b748ea63f2a9d130e0b2c430824682a96f6534be", size = 466733 }, { url = "https://files.pythonhosted.org/packages/ed/7e/e385e54fa3d9360f9d1ea502a5627f2f4bdd141dd227a1f8785335c4fca9/aiohttp-3.11.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e22d1721c978a6494adc824e0916f9d187fa57baeda34b55140315fa2f740184", size = 453993 }, { url = "https://files.pythonhosted.org/packages/ee/41/660cba8b4b10a9072ae77ce81558cca94d98aaec649a3085e50b8226fc17/aiohttp-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e993676c71288618eb07e20622572b1250d8713e7e00ab3aabae28cb70f3640d", size = 1576329 }, { url = "https://files.pythonhosted.org/packages/e1/51/4c59724afde127001b22cf09b28171829329cf2c838cb05f6de521f125cf/aiohttp-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e13a05db87d3b241c186d0936808d0e4e12decc267c617d54e9c643807e968b6", size = 1630344 }, { url = "https://files.pythonhosted.org/packages/c7/66/513f15cec950410dbc4439926ea4d9361136df7a97ddffab0deea1b68131/aiohttp-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ba8d043fed7ffa117024d7ba66fdea011c0e7602327c6d73cacaea38abe4491", size = 1666837 }, { url = "https://files.pythonhosted.org/packages/7a/c0/3e59d4cd8fd4c0e365d0ec962e0679dfc7629bdf0e67be398ca842ad4661/aiohttp-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda3ed0a7869d2fa16aa41f9961ade73aa2c2e3b2fcb0a352524e7b744881889", size = 1580628 }, { url = "https://files.pythonhosted.org/packages/22/a6/c4aea2cf583821e02f7a92c43f5f554d2334e22b741e21e8f31da2b2386b/aiohttp-3.11.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43bfd25113c1e98aec6c70e26d5f4331efbf4aa9037ba9ad88f090853bf64d7f", size = 1539922 }, { url = "https://files.pythonhosted.org/packages/7b/54/52f33fc9cecaf28f8400e92d9c22e37939c856c4a8af26a71023ec1de689/aiohttp-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3dd3e7e7c9ef3e7214f014f1ae260892286647b3cf7c7f1b644a568fd410f8ca", size = 1527342 }, { url = "https://files.pythonhosted.org/packages/d4/e0/fc91528bfb0283691b0448e93fe64d2416254a9ca34c58c666240440db89/aiohttp-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78c657ece7a73b976905ab9ec8be9ef2df12ed8984c24598a1791c58ce3b4ce4", size = 1534194 }, { url = "https://files.pythonhosted.org/packages/34/be/c6d571f46e9ef1720a850dce4c04dbfe38627a64bfdabdefb448c547e267/aiohttp-3.11.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:db70a47987e34494b451a334605bee57a126fe8d290511349e86810b4be53b01", size = 1609532 }, { url = "https://files.pythonhosted.org/packages/3d/af/1da6918c83fb427e0f23401dca03b8d6ec776fb61ad25d2f5a8d564418e6/aiohttp-3.11.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9e67531370a3b07e49b280c1f8c2df67985c790ad2834d1b288a2f13cd341c5f", size = 1630627 }, { url = "https://files.pythonhosted.org/packages/32/20/fd3f4d8bc60227f1eb2fc20e75679e270ef05f81ae618cd869a68f19a32c/aiohttp-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9202f184cc0582b1db15056f2225ab4c1e3dac4d9ade50dd0613ac3c46352ac2", size = 1565670 }, { url = "https://files.pythonhosted.org/packages/b0/9f/db692e10567acb0970618557be3bfe47fe92eac69fa7d3e81315d39b4a8b/aiohttp-3.11.7-cp310-cp310-win32.whl", hash = "sha256:2257bdd5cf54a4039a4337162cd8048f05a724380a2283df34620f55d4e29341", size = 415107 }, { url = "https://files.pythonhosted.org/packages/0b/8c/9fb539a8a773356df3dbddd77d4a3aff3eda448a602a90e5582d8b1903a4/aiohttp-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:b7215bf2b53bc6cb35808149980c2ae80a4ae4e273890ac85459c014d5aa60ac", size = 440569 }, { url = "https://files.pythonhosted.org/packages/13/7f/272fa1adf68fe2fbebfe686a67b50cfb40d86dfe47d0441aff6f0b7c4c0e/aiohttp-3.11.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cea52d11e02123f125f9055dfe0ccf1c3857225fb879e4a944fae12989e2aef2", size = 706820 }, { url = "https://files.pythonhosted.org/packages/79/3c/6d612ef77cdba75364393f04c5c577481e3b5123a774eea447ada1ddd14f/aiohttp-3.11.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ce18f703b7298e7f7633efd6a90138d99a3f9a656cb52c1201e76cb5d79cf08", size = 466654 }, { url = "https://files.pythonhosted.org/packages/4f/b8/1052667d4800cd49bb4f869f1ed42f5e9d5acd4676275e64ccc244c9c040/aiohttp-3.11.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:670847ee6aeb3a569cd7cdfbe0c3bec1d44828bbfbe78c5d305f7f804870ef9e", size = 454041 }, { url = "https://files.pythonhosted.org/packages/9f/07/80fa7302314a6ee1c9278550e9d95b77a4c895999bfbc5364ed0ee28dc7c/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dda726f89bfa5c465ba45b76515135a3ece0088dfa2da49b8bb278f3bdeea12", size = 1684778 }, { url = "https://files.pythonhosted.org/packages/2e/30/a71eb45197ad6bb6af87dfb39be8b56417d24d916047d35ef3f164af87f4/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25b74a811dba37c7ea6a14d99eb9402d89c8d739d50748a75f3cf994cf19c43", size = 1740992 }, { url = "https://files.pythonhosted.org/packages/22/74/0f9394429f3c4197129333a150a85cb2a642df30097a39dd41257f0b3bdc/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5522ee72f95661e79db691310290c4618b86dff2d9b90baedf343fd7a08bf79", size = 1781816 }, { url = "https://files.pythonhosted.org/packages/7f/1a/1e256b39179c98d16d53ac62f64bfcfe7c5b2c1e68b83cddd4165854524f/aiohttp-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fbf41a6bbc319a7816ae0f0177c265b62f2a59ad301a0e49b395746eb2a9884", size = 1676692 }, { url = "https://files.pythonhosted.org/packages/9b/37/f19d2e00efcabb9183b16bd91244de1d9c4ff7bf0fb5b8302e29a78f3286/aiohttp-3.11.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59ee1925b5a5efdf6c4e7be51deee93984d0ac14a6897bd521b498b9916f1544", size = 1619523 }, { url = "https://files.pythonhosted.org/packages/ae/3c/af50cf5e06b98783fd776f17077f7b7e755d461114af5d6744dc037fc3b0/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:24054fce8c6d6f33a3e35d1c603ef1b91bbcba73e3f04a22b4f2f27dac59b347", size = 1644084 }, { url = "https://files.pythonhosted.org/packages/c0/a6/4e0233b085cbf2b6de573515c1eddde82f1c1f17e69347e32a5a5f2617ff/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:351849aca2c6f814575c1a485c01c17a4240413f960df1bf9f5deb0003c61a53", size = 1648332 }, { url = "https://files.pythonhosted.org/packages/06/20/7062e76e7817318c421c0f9d7b650fb81aaecf6d2f3a9833805b45ec2ea8/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:12724f3a211fa243570e601f65a8831372caf1a149d2f1859f68479f07efec3d", size = 1730912 }, { url = "https://files.pythonhosted.org/packages/6c/1c/ff6ae4b1789894e6faf8a4e260cd3861cad618dc80ad15326789a7765750/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7ea4490360b605804bea8173d2d086b6c379d6bb22ac434de605a9cbce006e7d", size = 1752619 }, { url = "https://files.pythonhosted.org/packages/33/58/ddd5cba5ca245c00b04e9d28a7988b0f0eda02de494f8e62ecd2780655c2/aiohttp-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e0bf378db07df0a713a1e32381a1b277e62ad106d0dbe17b5479e76ec706d720", size = 1692801 }, { url = "https://files.pythonhosted.org/packages/b2/fc/32d5e2070b43d3722b7ea65ddc6b03ffa39bcc4b5ab6395a825cde0872ad/aiohttp-3.11.7-cp311-cp311-win32.whl", hash = "sha256:cd8d62cab363dfe713067027a5adb4907515861f1e4ce63e7be810b83668b847", size = 414899 }, { url = "https://files.pythonhosted.org/packages/ec/7e/50324c6d3df4540f5963def810b9927f220c99864065849a1dfcae77a6ce/aiohttp-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:bf0e6cce113596377cadda4e3ac5fb89f095bd492226e46d91b4baef1dd16f60", size = 440938 }, { url = "https://files.pythonhosted.org/packages/bf/1e/2e96b2526c590dcb99db0b94ac4f9b927ecc07f94735a8a941dee143d48b/aiohttp-3.11.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4bb7493c3e3a36d3012b8564bd0e2783259ddd7ef3a81a74f0dbfa000fce48b7", size = 702326 }, { url = "https://files.pythonhosted.org/packages/b5/ce/b5d7f3e68849f1f5e0b85af4ac9080b9d3c0a600857140024603653c2209/aiohttp-3.11.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e143b0ef9cb1a2b4f74f56d4fbe50caa7c2bb93390aff52f9398d21d89bc73ea", size = 461944 }, { url = "https://files.pythonhosted.org/packages/28/fa/f4d98db1b7f8f0c3f74bdbd6d0d98cfc89984205cd33f1b8ee3f588ee5ad/aiohttp-3.11.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7c58a240260822dc07f6ae32a0293dd5bccd618bb2d0f36d51c5dbd526f89c0", size = 454348 }, { url = "https://files.pythonhosted.org/packages/04/f0/c238dda5dc9a3d12b76636e2cf0ea475890ac3a1c7e4ff0fd6c3cea2fc2d/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d20cfe63a1c135d26bde8c1d0ea46fd1200884afbc523466d2f1cf517d1fe33", size = 1678795 }, { url = "https://files.pythonhosted.org/packages/79/ee/3a18f792247e6d95dba13aaedc9dc317c3c6e75f4b88c2dd4b960d20ad2f/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12e4d45847a174f77b2b9919719203769f220058f642b08504cf8b1cf185dacf", size = 1734411 }, { url = "https://files.pythonhosted.org/packages/f5/79/3eb84243087a9a32cae821622c935107b4b55a5b21b76772e8e6c41092e9/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf4efa2d01f697a7dbd0509891a286a4af0d86902fc594e20e3b1712c28c0106", size = 1788959 }, { url = "https://files.pythonhosted.org/packages/91/93/ad77782c5edfa17aafc070bef978fbfb8459b2f150595ffb01b559c136f9/aiohttp-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee6a4cdcbf54b8083dc9723cdf5f41f722c00db40ccf9ec2616e27869151129", size = 1687463 }, { url = "https://files.pythonhosted.org/packages/ba/48/db35bd21b7877efa0be5f28385d8978c55323c5ce7685712e53f3f6c0bd9/aiohttp-3.11.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6095aaf852c34f42e1bd0cf0dc32d1e4b48a90bfb5054abdbb9d64b36acadcb", size = 1618374 }, { url = "https://files.pythonhosted.org/packages/ba/77/30f87db55c79fd145ed5fd15b92f2e820ce81065d41ae437797aaa550e3b/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1cf03d27885f8c5ebf3993a220cc84fc66375e1e6e812731f51aab2b2748f4a6", size = 1637021 }, { url = "https://files.pythonhosted.org/packages/af/76/10b188b78ee18d0595af156d6a238bc60f9d8571f0f546027eb7eaf65b25/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1a17f6a230f81eb53282503823f59d61dff14fb2a93847bf0399dc8e87817307", size = 1650792 }, { url = "https://files.pythonhosted.org/packages/fa/33/4411bbb8ad04c47d0f4c7bd53332aaf350e49469cf6b65b132d4becafe27/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:481f10a1a45c5f4c4a578bbd74cff22eb64460a6549819242a87a80788461fba", size = 1696248 }, { url = "https://files.pythonhosted.org/packages/fe/2d/6135d0dc1851a33d3faa937b20fef81340bc95e8310536d4c7f1f8ecc026/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:db37248535d1ae40735d15bdf26ad43be19e3d93ab3f3dad8507eb0f85bb8124", size = 1729188 }, { url = "https://files.pythonhosted.org/packages/f5/76/a57ceff577ae26fe9a6f31ac799bc638ecf26e4acdf04295290b9929b349/aiohttp-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d18a8b44ec8502a7fde91446cd9c9b95ce7c49f1eacc1fb2358b8907d4369fd", size = 1690038 }, { url = "https://files.pythonhosted.org/packages/4b/81/b20e09003b6989a7f23a721692137a6143420a151063c750ab2a04878e3c/aiohttp-3.11.7-cp312-cp312-win32.whl", hash = "sha256:3d1c9c15d3999107cbb9b2d76ca6172e6710a12fda22434ee8bd3f432b7b17e8", size = 409887 }, { url = "https://files.pythonhosted.org/packages/b7/0b/607c98bff1d07bb21e0c39e7711108ef9ff4f2a361a3ec1ce8dce93623a5/aiohttp-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:018f1b04883a12e77e7fc161934c0f298865d3a484aea536a6a2ca8d909f0ba0", size = 436462 }, { url = "https://files.pythonhosted.org/packages/7a/53/8d77186c6a33bd087714df18274cdcf6e36fd69a9e841c85b7e81a20b18e/aiohttp-3.11.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:241a6ca732d2766836d62c58c49ca7a93d08251daef0c1e3c850df1d1ca0cbc4", size = 695811 }, { url = "https://files.pythonhosted.org/packages/62/b6/4c3d107a5406aa6f99f618afea82783f54ce2d9644020f50b9c88f6e823d/aiohttp-3.11.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa3705a8d14de39898da0fbad920b2a37b7547c3afd2a18b9b81f0223b7d0f68", size = 458530 }, { url = "https://files.pythonhosted.org/packages/d9/05/dbf0bd3966be8ebed3beb4007a2d1356d79af4fe7c93e54f984df6385193/aiohttp-3.11.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9acfc7f652b31853eed3b92095b0acf06fd5597eeea42e939bd23a17137679d5", size = 451371 }, { url = "https://files.pythonhosted.org/packages/19/6a/2198580314617b6cf9c4b813b84df5832b5f8efedcb8a7e8b321a187233c/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcefcf2915a2dbdbce37e2fc1622129a1918abfe3d06721ce9f6cdac9b6d2eaa", size = 1662905 }, { url = "https://files.pythonhosted.org/packages/2b/65/08696fd7503f6a6f9f782bd012bf47f36d4ed179a7d8c95dba4726d5cc67/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1f6490dd1862af5aae6cfcf2a274bffa9a5b32a8f5acb519a7ecf5a99a88866", size = 1713794 }, { url = "https://files.pythonhosted.org/packages/c8/a3/b9a72dce6f15e2efbc09fa67c1067c4f3a3bb05661c0ae7b40799cde02b7/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac5462582d6561c1c1708853a9faf612ff4e5ea5e679e99be36143d6eabd8e", size = 1770757 }, { url = "https://files.pythonhosted.org/packages/78/7e/8fb371b5f8c4c1eaa0d0a50750c0dd68059f86794aeb36919644815486f5/aiohttp-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1a6309005acc4b2bcc577ba3b9169fea52638709ffacbd071f3503264620da", size = 1673136 }, { url = "https://files.pythonhosted.org/packages/2f/0f/09685d13d2c7634cb808868ea29c170d4dcde4215a4a90fb86491cd3ae25/aiohttp-3.11.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b973cce96793725ef63eb449adfb74f99c043c718acb76e0d2a447ae369962", size = 1600370 }, { url = "https://files.pythonhosted.org/packages/00/2e/18fd38b117f9b3a375166ccb70ed43cf7e3dfe2cc947139acc15feefc5a2/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ce91a24aac80de6be8512fb1c4838a9881aa713f44f4e91dd7bb3b34061b497d", size = 1613459 }, { url = "https://files.pythonhosted.org/packages/2c/94/10a82abc680d753be33506be699aaa330152ecc4f316eaf081f996ee56c2/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:875f7100ce0e74af51d4139495eec4025affa1a605280f23990b6434b81df1bd", size = 1613924 }, { url = "https://files.pythonhosted.org/packages/e9/58/897c0561f5c522dda6e173192f1e4f10144e1a7126096f17a3f12b7aa168/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c171fc35d3174bbf4787381716564042a4cbc008824d8195eede3d9b938e29a8", size = 1681164 }, { url = "https://files.pythonhosted.org/packages/8b/8b/3a48b1cdafa612679d976274355f6a822de90b85d7dba55654ecfb01c979/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ee9afa1b0d2293c46954f47f33e150798ad68b78925e3710044e0d67a9487791", size = 1712139 }, { url = "https://files.pythonhosted.org/packages/aa/9d/70ab5b4dd7900db04af72840e033aee06e472b1343e372ea256ed675511c/aiohttp-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8360c7cc620abb320e1b8d603c39095101391a82b1d0be05fb2225471c9c5c52", size = 1667446 }, { url = "https://files.pythonhosted.org/packages/cb/98/b5fbcc8f6056f0c56001c75227e6b7ca9ee4f2e5572feca82ff3d65d485d/aiohttp-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7a9318da4b4ada9a67c1dd84d1c0834123081e746bee311a16bb449f363d965e", size = 408689 }, { url = "https://files.pythonhosted.org/packages/ef/07/4d1504577fa6349dd2e3839e89fb56e5dee38d64efe3d4366e9fcfda0cdb/aiohttp-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:fc6da202068e0a268e298d7cd09b6e9f3997736cd9b060e2750963754552a0a9", size = 434809 }, { url = "https://files.pythonhosted.org/packages/a1/51/5ad023409da8ca9f3edaa459bae95a65b9515a2eca8ea6510d2a87be1d53/aiohttp-3.11.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:17829f37c0d31d89aa6b8b010475a10233774771f9b6dc2cc352ea4f8ce95d9a", size = 707780 }, { url = "https://files.pythonhosted.org/packages/2f/74/94101af13b20325b60054a7dcc85f0eb50ea7750365ce0e5365494a6d4d7/aiohttp-3.11.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d6177077a31b1aecfc3c9070bd2f11419dbb4a70f30f4c65b124714f525c2e48", size = 467204 }, { url = "https://files.pythonhosted.org/packages/25/44/748d16ff174afad29452543d9c62101d8852a81e278d89a0fe73d81c99c1/aiohttp-3.11.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:badda65ac99555791eed75e234afb94686ed2317670c68bff8a4498acdaee935", size = 454491 }, { url = "https://files.pythonhosted.org/packages/c9/cc/f05d3d3f2bb68c0c41d31cabbd47fd019edf20c04a16de434a621ce17883/aiohttp-3.11.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0de6466b9d742b4ee56fe1b2440706e225eb48c77c63152b1584864a236e7a50", size = 1578119 }, { url = "https://files.pythonhosted.org/packages/81/f5/32ba5be33696d0a8f5cbf213d158a90d99b9b7d7b3d344c8400bb87364a6/aiohttp-3.11.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04b0cc74d5a882c9dacaeeccc1444f0233212b6f5be8bc90833feef1e1ce14b9", size = 1632860 }, { url = "https://files.pythonhosted.org/packages/eb/e7/23cc29b24d53c6d2ade7092f7d3cdc985c0414f00dfc81699bfa512c7968/aiohttp-3.11.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c7af3e50e5903d21d7b935aceed901cc2475463bc16ddd5587653548661fdb", size = 1670227 }, { url = "https://files.pythonhosted.org/packages/1a/48/51d3af146bb35988072d0456faadec603ac40e1d4974de07d1bf11065f2b/aiohttp-3.11.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c63f898f683d1379b9be5afc3dd139e20b30b0b1e0bf69a3fc3681f364cf1629", size = 1583960 }, { url = "https://files.pythonhosted.org/packages/66/fe/574c2cf9fa7e396c089fb34aaa121b91883a7c2b382043f471f13ca3fdd3/aiohttp-3.11.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdadc3f6a32d6eca45f9a900a254757fd7855dfb2d8f8dcf0e88f0fae3ff8eb1", size = 1539300 }, { url = "https://files.pythonhosted.org/packages/12/33/a7e88497a6775aa25baca2ec37f861ad1417e6113e685f89952986c232d7/aiohttp-3.11.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d329300fb23e14ed1f8c6d688dfd867d1dcc3b1d7cd49b7f8c5b44e797ce0932", size = 1524716 }, { url = "https://files.pythonhosted.org/packages/fd/3a/ddcdd768c8302cdecf411fde591c2b93ab180d7cc3a61fbed86f025075ee/aiohttp-3.11.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5578cf40440eafcb054cf859964bc120ab52ebe0e0562d2b898126d868749629", size = 1534492 }, { url = "https://files.pythonhosted.org/packages/85/f8/dd77ad1e4da943d633bc950fed565d14e82bbe5b7ffc4832f106c69396af/aiohttp-3.11.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7b2f8107a3c329789f3c00b2daad0e35f548d0a55cda6291579136622099a46e", size = 1608164 }, { url = "https://files.pythonhosted.org/packages/5c/34/e3e41dafe6e4c9032f1b1d8130aa0f023275b3398d6887e94fbd68731ba7/aiohttp-3.11.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:43dd89a6194f6ab02a3fe36b09e42e2df19c211fc2050ce37374d96f39604997", size = 1627119 }, { url = "https://files.pythonhosted.org/packages/f6/99/5746e91be936a78c73b52549eefb462ae521c0053f19de08335f52896d75/aiohttp-3.11.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d2fa6fc7cc865d26ff42480ac9b52b8c9b7da30a10a6442a9cdf429de840e949", size = 1564248 }, { url = "https://files.pythonhosted.org/packages/dc/df/5b2c8e243acaa2433baaf8431dd23d90840ccecd0755c2bccde4e8da85d9/aiohttp-3.11.7-cp39-cp39-win32.whl", hash = "sha256:a7d9a606355655617fee25dd7e54d3af50804d002f1fd3118dd6312d26692d70", size = 415407 }, { url = "https://files.pythonhosted.org/packages/f8/23/1f34e9cee17ebb0202cf9bec9e2eaf8e7e4f4ac36d12c9ab3786b19679f8/aiohttp-3.11.7-cp39-cp39-win_amd64.whl", hash = "sha256:53c921b58fdc6485d6b2603e0132bb01cd59b8f0620ffc0907f525e0ba071687", size = 440807 }, ] [[package]] name = "aioresponses" version = "0.7.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/eb/a69466280306dc9976687cda06d2c9195ff72533192184627f5e7b1d3f1e/aioresponses-0.7.7.tar.gz", hash = "sha256:66292f1d5c94a3cb984f3336d806446042adb17347d3089f2d3962dd6e5ba55a", size = 39087 } wheels = [ { url = "https://files.pythonhosted.org/packages/92/23/04a00b3714803e5a58f893eec230b58956e1e8289d3e223d9e294dac3cda/aioresponses-0.7.7-py2.py3-none-any.whl", hash = "sha256:6975f31fe5e7f2113a41bd387221f31854f285ecbc05527272cd8ba4c50764a3", size = 12152 }, ] [[package]] name = "aiosignal" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, ] [[package]] name = "alabaster" version = "0.7.16" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } wheels = [ { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, ] [[package]] name = "anyio" version = "4.6.2.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, ] [[package]] name = "async-timeout" version = "5.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, ] [[package]] name = "asyncclick" version = "8.1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "colorama", marker = "platform_system == 'Windows'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/bf/59d836c3433d7aa07f76c2b95c4eb763195ea8a5d7f9ad3311ed30c2af61/asyncclick-8.1.7.2.tar.gz", hash = "sha256:219ea0f29ccdc1bb4ff43bcab7ce0769ac6d48a04f997b43ec6bee99a222daa0", size = 349073 } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/6e/9acdbb25733e1de411663b59abe521bec738e72fe4e85843f6ff8b212832/asyncclick-8.1.7.2-py3-none-any.whl", hash = "sha256:1ab940b04b22cb89b5b400725132b069d01b0c3472a9702c7a2c9d5d007ded02", size = 99191 }, ] [[package]] name = "attrs" version = "24.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, ] [[package]] name = "babel" version = "2.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, ] [[package]] name = "certifi" version = "2024.8.30" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } wheels = [ { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, ] [[package]] name = "cffi" version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } wheels = [ { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, ] [[package]] name = "charset-normalizer" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } wheels = [ { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "coverage" version = "7.6.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ab/75/aecfd0a3adbec6e45753976bc2a9fed62b42cea9a206d10fd29244a77953/coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", size = 801425 } wheels = [ { url = "https://files.pythonhosted.org/packages/31/86/6ed22e101badc8eedf181f0c2f65500df5929c44c79991cf45b9bf741424/coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50", size = 206988 }, { url = "https://files.pythonhosted.org/packages/3b/04/16853c58bacc02b3ff5405193dfc6c66632442d931b23dd7b9452dc55cf3/coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf", size = 207418 }, { url = "https://files.pythonhosted.org/packages/f8/eb/8a91520d04215eb549d6a7d7d3a79cbb1d78b5dd0814f4b23bf97521d580/coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee", size = 235860 }, { url = "https://files.pythonhosted.org/packages/00/10/bf1ede5b54ae1bbf39921a5dd4cc84aee79041ed301ec8955064785ddb90/coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6", size = 233766 }, { url = "https://files.pythonhosted.org/packages/5c/ea/741d9233eb502906e0d18ccf4c15c4fb74ff0e85fd8ee967590194b889a1/coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d", size = 234924 }, { url = "https://files.pythonhosted.org/packages/18/43/b2cfd4413a5b64ab27c289228b0c45b4527d1b99381cc9d6a00bfd515da4/coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331", size = 234019 }, { url = "https://files.pythonhosted.org/packages/8e/95/8b2fbb9d1a79277963b6095cd51a90fb7088cd3618faf75550038331f78b/coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638", size = 232481 }, { url = "https://files.pythonhosted.org/packages/4d/d7/9e939508a39ef67605b715ca89c6522214aceb27c2db9152ae3ae1cf8626/coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed", size = 233609 }, { url = "https://files.pythonhosted.org/packages/ba/e2/1c5fb52eafcffeebaa9db084bff47e7c3cf4f97db752226c232cee4d530b/coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e", size = 209669 }, { url = "https://files.pythonhosted.org/packages/31/31/6a56469609a252549dd4b090815428d5521edd4642440d987573a450c069/coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a", size = 210509 }, { url = "https://files.pythonhosted.org/packages/ab/9f/e98211980f6e2f439e251737482aa77906c9b9c507824c71a2ce7eea0402/coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", size = 207093 }, { url = "https://files.pythonhosted.org/packages/fd/c7/8bab83fb9c20f7f8163c5a20dcb62d591b906a214a6dc6b07413074afc80/coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", size = 207536 }, { url = "https://files.pythonhosted.org/packages/1e/d6/00243df625f1b282bb25c83ce153ae2c06f8e7a796a8d833e7235337b4d9/coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", size = 239482 }, { url = "https://files.pythonhosted.org/packages/1e/07/faf04b3eeb55ffc2a6f24b65dffe6e0359ec3b283e6efb5050ea0707446f/coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", size = 236886 }, { url = "https://files.pythonhosted.org/packages/43/23/c79e497bf4d8fcacd316bebe1d559c765485b8ec23ac4e23025be6bfce09/coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", size = 238749 }, { url = "https://files.pythonhosted.org/packages/b5/e5/791bae13be3c6451e32ef7af1192e711c6a319f3c597e9b218d148fd0633/coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", size = 237679 }, { url = "https://files.pythonhosted.org/packages/05/c6/bbfdfb03aada601fb8993ced17468c8c8e0b4aafb3097026e680fabb7ce1/coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", size = 236317 }, { url = "https://files.pythonhosted.org/packages/67/f9/f8e5a4b2ce96d1b0e83ae6246369eb8437001dc80ec03bb51c87ff557cd8/coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", size = 237084 }, { url = "https://files.pythonhosted.org/packages/f0/70/b05328901e4debe76e033717e1452d00246c458c44e9dbd893e7619c2967/coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", size = 209638 }, { url = "https://files.pythonhosted.org/packages/70/55/1efa24f960a2fa9fbc44a9523d3f3c50ceb94dd1e8cd732168ab2dc41b07/coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9", size = 210506 }, { url = "https://files.pythonhosted.org/packages/76/ce/3edf581c8fe429ed8ced6e6d9ac693c25975ef9093413276dab6ed68a80a/coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee", size = 207285 }, { url = "https://files.pythonhosted.org/packages/09/9c/cf102ab046c9cf8895c3f7aadcde6f489a4b2ec326757e8c6e6581829b5e/coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a", size = 207522 }, { url = "https://files.pythonhosted.org/packages/39/06/42aa6dd13dbfca72e1fd8ffccadbc921b6e75db34545ebab4d955d1e7ad3/coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d", size = 240543 }, { url = "https://files.pythonhosted.org/packages/a0/20/2932971dc215adeca8eeff446266a7fef17a0c238e881ffedebe7bfa0669/coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb", size = 237577 }, { url = "https://files.pythonhosted.org/packages/ac/85/4323ece0cd5452c9522f4b6e5cc461e6c7149a4b1887c9e7a8b1f4e51146/coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649", size = 239646 }, { url = "https://files.pythonhosted.org/packages/77/52/b2537487d8f36241e518e84db6f79e26bc3343b14844366e35b090fae0d4/coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787", size = 239128 }, { url = "https://files.pythonhosted.org/packages/7c/99/7f007762012186547d0ecc3d328da6b6f31a8c99f05dc1e13dcd929918cd/coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c", size = 237434 }, { url = "https://files.pythonhosted.org/packages/97/53/e9b5cf0682a1cab9352adfac73caae0d77ae1d65abc88975d510f7816389/coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443", size = 239095 }, { url = "https://files.pythonhosted.org/packages/0c/50/054f0b464fbae0483217186478eefa2e7df3a79917ed7f1d430b6da2cf0d/coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad", size = 209895 }, { url = "https://files.pythonhosted.org/packages/df/d0/09ba870360a27ecf09e177ca2ff59d4337fc7197b456f22ceff85cffcfa5/coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4", size = 210684 }, { url = "https://files.pythonhosted.org/packages/9a/84/6f0ccf94a098ac3d6d6f236bd3905eeac049a9e0efcd9a63d4feca37ac4b/coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb", size = 207313 }, { url = "https://files.pythonhosted.org/packages/db/2b/e3b3a3a12ebec738c545897ac9f314620470fcbc368cdac88cf14974ba20/coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63", size = 207574 }, { url = "https://files.pythonhosted.org/packages/db/c0/5bf95d42b6a8d21dfce5025ce187f15db57d6460a59b67a95fe8728162f1/coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365", size = 240090 }, { url = "https://files.pythonhosted.org/packages/57/b8/d6fd17d1a8e2b0e1a4e8b9cb1f0f261afd422570735899759c0584236916/coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002", size = 237237 }, { url = "https://files.pythonhosted.org/packages/d4/e4/a91e9bb46809c8b63e68fc5db5c4d567d3423b6691d049a4f950e38fbe9d/coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3", size = 239225 }, { url = "https://files.pythonhosted.org/packages/31/9c/9b99b0591ec4555b7292d271e005f27b465388ce166056c435b288db6a69/coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022", size = 238888 }, { url = "https://files.pythonhosted.org/packages/a6/85/285c2df9a04bc7c31f21fd9d4a24d19e040ec5e2ff06e572af1f6514c9e7/coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e", size = 236974 }, { url = "https://files.pythonhosted.org/packages/cb/a1/95ec8522206f76cdca033bf8bb61fff56429fb414835fc4d34651dfd29fc/coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b", size = 238815 }, { url = "https://files.pythonhosted.org/packages/8d/ac/687e9ba5e6d0979e9dab5c02e01c4f24ac58260ef82d88d3b433b3f84f1e/coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146", size = 209957 }, { url = "https://files.pythonhosted.org/packages/2f/a3/b61cc8e3fcf075293fb0f3dee405748453c5ba28ac02ceb4a87f52bdb105/coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28", size = 210711 }, { url = "https://files.pythonhosted.org/packages/ee/4b/891c8b9acf1b62c85e4a71dac142ab9284e8347409b7355de02e3f38306f/coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d", size = 208053 }, { url = "https://files.pythonhosted.org/packages/18/a9/9e330409b291cc002723d339346452800e78df1ce50774ca439ade1d374f/coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451", size = 208329 }, { url = "https://files.pythonhosted.org/packages/9c/0d/33635fd429f6589c6e1cdfc7bf581aefe4c1792fbff06383f9d37f59db60/coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764", size = 251052 }, { url = "https://files.pythonhosted.org/packages/23/32/8a08da0e46f3830bbb9a5b40614241b2e700f27a9c2889f53122486443ed/coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf", size = 246765 }, { url = "https://files.pythonhosted.org/packages/56/3f/3b86303d2c14350fdb1c6c4dbf9bc76000af2382f42ca1d4d99c6317666e/coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5", size = 249125 }, { url = "https://files.pythonhosted.org/packages/36/cb/c4f081b9023f9fd8646dbc4ef77be0df090263e8f66f4ea47681e0dc2cff/coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4", size = 248615 }, { url = "https://files.pythonhosted.org/packages/32/ee/53bdbf67760928c44b57b2c28a8c0a4bf544f85a9ee129a63ba5c78fdee4/coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83", size = 246507 }, { url = "https://files.pythonhosted.org/packages/57/49/5a57910bd0af6d8e802b4ca65292576d19b54b49f81577fd898505dee075/coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b", size = 247785 }, { url = "https://files.pythonhosted.org/packages/bd/37/e450c9f6b297c79bb9858407396ed3e084dcc22990dd110ab01d5ceb9770/coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71", size = 210605 }, { url = "https://files.pythonhosted.org/packages/44/79/7d0c7dd237c6905018e2936cd1055fe1d42e7eba2ebab3c00f4aad2a27d7/coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc", size = 211777 }, { url = "https://files.pythonhosted.org/packages/2e/db/5c7008bcd8858c2dea02702ef0fee761f23780a6be7cd1292840f3e165b1/coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e", size = 206983 }, { url = "https://files.pythonhosted.org/packages/1c/30/e1be5b6802baa55967e83bdf57bd51cd2763b72cdc591a90aa0b465fffee/coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c", size = 207422 }, { url = "https://files.pythonhosted.org/packages/f6/df/19c0e12f9f7b976cd7b92ae8200d26f5b6cd3f322d17ac7b08d48fbf5bc5/coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0", size = 235455 }, { url = "https://files.pythonhosted.org/packages/e8/7a/a80b0c4fb48e8bce92bcfe3908e47e6c7607fb8f618a4e0de78218e42d9b/coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779", size = 233376 }, { url = "https://files.pythonhosted.org/packages/8c/0e/1a4ecee734d70b78fc458ff611707f804605721467ef45fc1f1a684772ad/coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92", size = 234509 }, { url = "https://files.pythonhosted.org/packages/24/42/6eadd73adc0163cb18dee4fef80baefeb3faa11a1e217a2db80e274e784d/coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4", size = 233659 }, { url = "https://files.pythonhosted.org/packages/68/5f/10b825f39ecfe6fc5ee3120205daaa0950443948f0d0a538430f386fdf58/coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc", size = 232138 }, { url = "https://files.pythonhosted.org/packages/56/72/ad92bdad934de103e19a128a349ef4a0560892fd33d62becb1140885e44c/coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea", size = 233131 }, { url = "https://files.pythonhosted.org/packages/f4/1d/d61d9b2d17628c4db834e9650b776663535b4258d0dc204ec475188b5b2a/coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e", size = 209695 }, { url = "https://files.pythonhosted.org/packages/0f/d1/ef43852a998c41183dbffed4ab0dd658f9975d570c6106ea43fdcb5dcbf4/coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076", size = 210475 }, { url = "https://files.pythonhosted.org/packages/32/df/0d2476121cd0bfb9ca2413efe02289c474b82c4b134863bef4b89ec7bcfa/coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce", size = 199230 }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "cryptography" version = "43.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } wheels = [ { url = "https://files.pythonhosted.org/packages/1f/f3/01fdf26701a26f4b4dbc337a26883ad5bccaa6f1bbbdd29cd89e22f18a1c/cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", size = 6225303 }, { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, { url = "https://files.pythonhosted.org/packages/56/48/7b6b190f1462818b324e674fa20d1d5ef3e24f2328675b9b16189cbf0b3c/cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", size = 2623873 }, { url = "https://files.pythonhosted.org/packages/eb/b1/0ebff61a004f7f89e7b65ca95f2f2375679d43d0290672f7713ee3162aff/cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", size = 3068039 }, { url = "https://files.pythonhosted.org/packages/30/d5/c8b32c047e2e81dd172138f772e81d852c51f0f2ad2ae8a24f1122e9e9a7/cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", size = 6222984 }, { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, { url = "https://files.pythonhosted.org/packages/81/1e/ffcc41b3cebd64ca90b28fd58141c5f68c83d48563c88333ab660e002cd3/cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", size = 2623145 }, { url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 }, { url = "https://files.pythonhosted.org/packages/6f/db/d8b8a039483f25fc3b70c90bc8f3e1d4497a99358d610c5067bf3bd4f0af/cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", size = 3144545 }, { url = "https://files.pythonhosted.org/packages/93/90/116edd5f8ec23b2dc879f7a42443e073cdad22950d3c8ee834e3b8124543/cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", size = 3679828 }, { url = "https://files.pythonhosted.org/packages/d8/32/1e1d78b316aa22c0ba6493cc271c1c309969e5aa5c22c830a1d7ce3471e6/cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", size = 3908132 }, { url = "https://files.pythonhosted.org/packages/91/bb/cd2c13be3332e7af3cdf16154147952d39075b9f61ea5e6b5241bf4bf436/cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", size = 2988811 }, { url = "https://files.pythonhosted.org/packages/cc/fc/ff7c76afdc4f5933b5e99092528d4783d3d1b131960fc8b31eb38e076ca8/cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", size = 3146844 }, { url = "https://files.pythonhosted.org/packages/d7/29/a233efb3e98b13d9175dcb3c3146988ec990896c8fa07e8467cce27d5a80/cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", size = 3681997 }, { url = "https://files.pythonhosted.org/packages/c0/cf/c9eea7791b961f279fb6db86c3355cfad29a73141f46427af71852b23b95/cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", size = 3905208 }, { url = "https://files.pythonhosted.org/packages/21/ea/6c38ca546d5b6dab3874c2b8fc6b1739baac29bacdea31a8c6c0513b3cfa/cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", size = 2989787 }, ] [[package]] name = "distlib" version = "0.3.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, ] [[package]] name = "docutils" version = "0.18.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/57/b1/b880503681ea1b64df05106fc7e3c4e3801736cf63deffc6fa7fc5404cf5/docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06", size = 2043249 } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/14/69b4bad34e3f250afe29a854da03acb6747711f3df06c359fa053fae4e76/docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", size = 570050 }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } wheels = [ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, ] [[package]] name = "filelock" version = "3.16.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } wheels = [ { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, ] [[package]] name = "firebase-messaging" version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "cryptography" }, { name = "http-ece" }, { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/20/9e/038c82e44bebfac9f865e9ca249a6c1bea382fd7fd6c409543fb89133b3b/firebase_messaging-0.4.4.tar.gz", hash = "sha256:b9593e2340442a08a4c3641dacbc54a1e688431027d2fd63a7e92efe3ec11ae8", size = 39375 } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/c1/c48b37bf53b908a6fe7c98282729762d0505077ce28306543c0e879c1a00/firebase_messaging-0.4.4-py3-none-any.whl", hash = "sha256:43aa4bd9014674006ec0462464b2caa85f70f7a8c19c407ba618b15bc8d6ff0f", size = 40017 }, ] [[package]] name = "freezegun" version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2c/ef/722b8d71ddf4d48f25f6d78aa2533d505bf3eec000a7cacb8ccc8de61f2f/freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", size = 33697 } wheels = [ { url = "https://files.pythonhosted.org/packages/51/0b/0d7fee5919bccc1fdc1c2a7528b98f65c6f69b223a3fd8f809918c142c36/freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1", size = 17569 }, ] [[package]] name = "frozenlist" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } wheels = [ { url = "https://files.pythonhosted.org/packages/54/79/29d44c4af36b2b240725dce566b20f63f9b36ef267aaaa64ee7466f4f2f8/frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", size = 94451 }, { url = "https://files.pythonhosted.org/packages/47/47/0c999aeace6ead8a44441b4f4173e2261b18219e4ad1fe9a479871ca02fc/frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", size = 54301 }, { url = "https://files.pythonhosted.org/packages/8d/60/107a38c1e54176d12e06e9d4b5d755b677d71d1219217cee063911b1384f/frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", size = 52213 }, { url = "https://files.pythonhosted.org/packages/17/62/594a6829ac5679c25755362a9dc93486a8a45241394564309641425d3ff6/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", size = 240946 }, { url = "https://files.pythonhosted.org/packages/7e/75/6c8419d8f92c80dd0ee3f63bdde2702ce6398b0ac8410ff459f9b6f2f9cb/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", size = 264608 }, { url = "https://files.pythonhosted.org/packages/88/3e/82a6f0b84bc6fb7e0be240e52863c6d4ab6098cd62e4f5b972cd31e002e8/frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", size = 261361 }, { url = "https://files.pythonhosted.org/packages/fd/85/14e5f9ccac1b64ff2f10c927b3ffdf88772aea875882406f9ba0cec8ad84/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", size = 231649 }, { url = "https://files.pythonhosted.org/packages/ee/59/928322800306f6529d1852323014ee9008551e9bb027cc38d276cbc0b0e7/frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", size = 241853 }, { url = "https://files.pythonhosted.org/packages/7d/bd/e01fa4f146a6f6c18c5d34cab8abdc4013774a26c4ff851128cd1bd3008e/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", size = 243652 }, { url = "https://files.pythonhosted.org/packages/a5/bd/e4771fd18a8ec6757033f0fa903e447aecc3fbba54e3630397b61596acf0/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", size = 241734 }, { url = "https://files.pythonhosted.org/packages/21/13/c83821fa5544af4f60c5d3a65d054af3213c26b14d3f5f48e43e5fb48556/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", size = 260959 }, { url = "https://files.pythonhosted.org/packages/71/f3/1f91c9a9bf7ed0e8edcf52698d23f3c211d8d00291a53c9f115ceb977ab1/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", size = 262706 }, { url = "https://files.pythonhosted.org/packages/4c/22/4a256fdf5d9bcb3ae32622c796ee5ff9451b3a13a68cfe3f68e2c95588ce/frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", size = 250401 }, { url = "https://files.pythonhosted.org/packages/af/89/c48ebe1f7991bd2be6d5f4ed202d94960c01b3017a03d6954dd5fa9ea1e8/frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", size = 45498 }, { url = "https://files.pythonhosted.org/packages/28/2f/cc27d5f43e023d21fe5c19538e08894db3d7e081cbf582ad5ed366c24446/frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", size = 51622 }, { url = "https://files.pythonhosted.org/packages/79/43/0bed28bf5eb1c9e4301003b74453b8e7aa85fb293b31dde352aac528dafc/frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", size = 94987 }, { url = "https://files.pythonhosted.org/packages/bb/bf/b74e38f09a246e8abbe1e90eb65787ed745ccab6eaa58b9c9308e052323d/frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", size = 54584 }, { url = "https://files.pythonhosted.org/packages/2c/31/ab01375682f14f7613a1ade30149f684c84f9b8823a4391ed950c8285656/frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", size = 52499 }, { url = "https://files.pythonhosted.org/packages/98/a8/d0ac0b9276e1404f58fec3ab6e90a4f76b778a49373ccaf6a563f100dfbc/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", size = 276357 }, { url = "https://files.pythonhosted.org/packages/ad/c9/c7761084fa822f07dac38ac29f841d4587570dd211e2262544aa0b791d21/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", size = 287516 }, { url = "https://files.pythonhosted.org/packages/a1/ff/cd7479e703c39df7bdab431798cef89dc75010d8aa0ca2514c5b9321db27/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", size = 283131 }, { url = "https://files.pythonhosted.org/packages/59/a0/370941beb47d237eca4fbf27e4e91389fd68699e6f4b0ebcc95da463835b/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", size = 261320 }, { url = "https://files.pythonhosted.org/packages/b8/5f/c10123e8d64867bc9b4f2f510a32042a306ff5fcd7e2e09e5ae5100ee333/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", size = 274877 }, { url = "https://files.pythonhosted.org/packages/fa/79/38c505601ae29d4348f21706c5d89755ceded02a745016ba2f58bd5f1ea6/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", size = 269592 }, { url = "https://files.pythonhosted.org/packages/19/e2/39f3a53191b8204ba9f0bb574b926b73dd2efba2a2b9d2d730517e8f7622/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", size = 265934 }, { url = "https://files.pythonhosted.org/packages/d5/c9/3075eb7f7f3a91f1a6b00284af4de0a65a9ae47084930916f5528144c9dd/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", size = 283859 }, { url = "https://files.pythonhosted.org/packages/05/f5/549f44d314c29408b962fa2b0e69a1a67c59379fb143b92a0a065ffd1f0f/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", size = 287560 }, { url = "https://files.pythonhosted.org/packages/9d/f8/cb09b3c24a3eac02c4c07a9558e11e9e244fb02bf62c85ac2106d1eb0c0b/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", size = 277150 }, { url = "https://files.pythonhosted.org/packages/37/48/38c2db3f54d1501e692d6fe058f45b6ad1b358d82cd19436efab80cfc965/frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", size = 45244 }, { url = "https://files.pythonhosted.org/packages/ca/8c/2ddffeb8b60a4bce3b196c32fcc30d8830d4615e7b492ec2071da801b8ad/frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", size = 51634 }, { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", size = 283591 }, { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", size = 267026 }, { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, { url = "https://files.pythonhosted.org/packages/da/4d/d94ff0fb0f5313902c132817c62d19cdc5bdcd0c195d392006ef4b779fc6/frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972", size = 95319 }, { url = "https://files.pythonhosted.org/packages/8c/1b/d90e554ca2b483d31cb2296e393f72c25bdc38d64526579e95576bfda587/frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336", size = 54749 }, { url = "https://files.pythonhosted.org/packages/f8/66/7fdecc9ef49f8db2aa4d9da916e4ecf357d867d87aea292efc11e1b2e932/frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f", size = 52718 }, { url = "https://files.pythonhosted.org/packages/08/04/e2fddc92135276e07addbc1cf413acffa0c2d848b3e54cacf684e146df49/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f", size = 241756 }, { url = "https://files.pythonhosted.org/packages/c6/52/be5ff200815d8a341aee5b16b6b707355e0ca3652953852238eb92b120c2/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6", size = 267718 }, { url = "https://files.pythonhosted.org/packages/88/be/4bd93a58be57a3722fc544c36debdf9dcc6758f761092e894d78f18b8f20/frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411", size = 263494 }, { url = "https://files.pythonhosted.org/packages/32/ba/58348b90193caa096ce9e9befea6ae67f38dabfd3aacb47e46137a6250a8/frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08", size = 232838 }, { url = "https://files.pythonhosted.org/packages/f6/33/9f152105227630246135188901373c4f322cc026565ca6215b063f4c82f4/frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2", size = 242912 }, { url = "https://files.pythonhosted.org/packages/a0/10/3db38fb3ccbafadd80a1b0d6800c987b0e3fe3ef2d117c6ced0246eea17a/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d", size = 244763 }, { url = "https://files.pythonhosted.org/packages/e2/cd/1df468fdce2f66a4608dffe44c40cdc35eeaa67ef7fd1d813f99a9a37842/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b", size = 242841 }, { url = "https://files.pythonhosted.org/packages/ee/5f/16097a5ca0bb6b6779c02cc9379c72fe98d56115d4c54d059fb233168fb6/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b", size = 263407 }, { url = "https://files.pythonhosted.org/packages/0f/f7/58cd220ee1c2248ee65a32f5b4b93689e3fe1764d85537eee9fc392543bc/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0", size = 265083 }, { url = "https://files.pythonhosted.org/packages/62/b8/49768980caabf81ac4a2d156008f7cbd0107e6b36d08a313bb31035d9201/frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c", size = 251564 }, { url = "https://files.pythonhosted.org/packages/cb/83/619327da3b86ef957ee7a0cbf3c166a09ed1e87a3f7f1ff487d7d0284683/frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3", size = 45691 }, { url = "https://files.pythonhosted.org/packages/8b/28/407bc34a745151ed2322c690b6e7d83d7101472e81ed76e1ebdac0b70a78/frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0", size = 51767 }, { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, ] [[package]] name = "http-ece" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/1a/60ccc29fccd4789b7cada188b114185e8a5d63aba0d93262adbbe776cfe5/http_ece-1.1.0.tar.gz", hash = "sha256:932ebc2fa7c216954c320a188ae9c1f04d01e67bec9cdce1bfbc912813b0b4f8", size = 4902 } [[package]] name = "identify" version = "2.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1a/5f/05f0d167be94585d502b4adf8c7af31f1dc0b1c7e14f9938a88fdbbcf4a7/identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02", size = 99179 } wheels = [ { url = "https://files.pythonhosted.org/packages/c9/f5/09644a3ad803fae9eca8efa17e1f2aef380c7f0b02f7ec4e8d446e51d64a/identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd", size = 99049 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "imagesize" version = "1.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, ] [[package]] name = "importlib-metadata" version = "8.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] name = "jinja2" version = "3.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } wheels = [ { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, ] [[package]] name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } wheels = [ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 }, { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 }, { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 }, { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 }, { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 }, { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 }, { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 }, { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 }, { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 }, { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, ] [[package]] name = "mdit-py-plugins" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] [[package]] name = "mock" version = "5.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/66/ab/41d09a46985ead5839d8be987acda54b5bb93f713b3969cc0be4f81c455b/mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d", size = 80232 } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/20/471f41173930550f279ccb65596a5ac19b9ac974a8d93679bcd3e0c31498/mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744", size = 30938 }, ] [[package]] name = "multidict" version = "6.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } wheels = [ { url = "https://files.pythonhosted.org/packages/29/68/259dee7fd14cf56a17c554125e534f6274c2860159692a414d0b402b9a6d/multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", size = 48628 }, { url = "https://files.pythonhosted.org/packages/50/79/53ba256069fe5386a4a9e80d4e12857ced9de295baf3e20c68cdda746e04/multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", size = 29327 }, { url = "https://files.pythonhosted.org/packages/ff/10/71f1379b05b196dae749b5ac062e87273e3f11634f447ebac12a571d90ae/multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", size = 29689 }, { url = "https://files.pythonhosted.org/packages/71/45/70bac4f87438ded36ad4793793c0095de6572d433d98575a5752629ef549/multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", size = 126639 }, { url = "https://files.pythonhosted.org/packages/80/cf/17f35b3b9509b4959303c05379c4bfb0d7dd05c3306039fc79cf035bbac0/multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", size = 134315 }, { url = "https://files.pythonhosted.org/packages/ef/1f/652d70ab5effb33c031510a3503d4d6efc5ec93153562f1ee0acdc895a57/multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", size = 129471 }, { url = "https://files.pythonhosted.org/packages/a6/64/2dd6c4c681688c0165dea3975a6a4eab4944ea30f35000f8b8af1df3148c/multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", size = 124585 }, { url = "https://files.pythonhosted.org/packages/87/56/e6ee5459894c7e554b57ba88f7257dc3c3d2d379cb15baaa1e265b8c6165/multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", size = 116957 }, { url = "https://files.pythonhosted.org/packages/36/9e/616ce5e8d375c24b84f14fc263c7ef1d8d5e8ef529dbc0f1df8ce71bb5b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db", size = 128609 }, { url = "https://files.pythonhosted.org/packages/8c/4f/4783e48a38495d000f2124020dc96bacc806a4340345211b1ab6175a6cb4/multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", size = 123016 }, { url = "https://files.pythonhosted.org/packages/3e/b3/4950551ab8fc39862ba5e9907dc821f896aa829b4524b4deefd3e12945ab/multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", size = 133542 }, { url = "https://files.pythonhosted.org/packages/96/4d/f0ce6ac9914168a2a71df117935bb1f1781916acdecbb43285e225b484b8/multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", size = 130163 }, { url = "https://files.pythonhosted.org/packages/be/72/17c9f67e7542a49dd252c5ae50248607dfb780bcc03035907dafefb067e3/multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", size = 126832 }, { url = "https://files.pythonhosted.org/packages/71/9f/72d719e248cbd755c8736c6d14780533a1606ffb3fbb0fbd77da9f0372da/multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", size = 26402 }, { url = "https://files.pythonhosted.org/packages/04/5a/d88cd5d00a184e1ddffc82aa2e6e915164a6d2641ed3606e766b5d2f275a/multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", size = 28800 }, { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, { url = "https://files.pythonhosted.org/packages/e7/c9/9e153a6572b38ac5ff4434113af38acf8d5e9957897cdb1f513b3d6614ed/multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c", size = 48550 }, { url = "https://files.pythonhosted.org/packages/76/f5/79565ddb629eba6c7f704f09a09df085c8dc04643b12506f10f718cee37a/multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1", size = 29298 }, { url = "https://files.pythonhosted.org/packages/60/1b/9851878b704bc98e641a3e0bce49382ae9e05743dac6d97748feb5b7baba/multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c", size = 29641 }, { url = "https://files.pythonhosted.org/packages/89/87/d451d45aab9e422cb0fb2f7720c31a4c1d3012c740483c37f642eba568fb/multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c", size = 126202 }, { url = "https://files.pythonhosted.org/packages/fa/b4/27cbe9f3e2e469359887653f2e45470272eef7295139916cc21107c6b48c/multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f", size = 133925 }, { url = "https://files.pythonhosted.org/packages/4d/a3/afc841899face8adfd004235ce759a37619f6ec99eafd959650c5ce4df57/multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875", size = 129039 }, { url = "https://files.pythonhosted.org/packages/5e/41/0d0fb18c1ad574f807196f5f3d99164edf9de3e169a58c6dc2d6ed5742b9/multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255", size = 124072 }, { url = "https://files.pythonhosted.org/packages/00/22/defd7a2e71a44e6e5b9a5428f972e5b572e7fe28e404dfa6519bbf057c93/multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30", size = 116532 }, { url = "https://files.pythonhosted.org/packages/91/25/f7545102def0b1d456ab6449388eed2dfd822debba1d65af60194904a23a/multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057", size = 128173 }, { url = "https://files.pythonhosted.org/packages/45/79/3dbe8d35fc99f5ea610813a72ab55f426cb9cf482f860fa8496e5409be11/multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657", size = 122654 }, { url = "https://files.pythonhosted.org/packages/97/cb/209e735eeab96e1b160825b5d0b36c56d3862abff828fc43999bb957dcad/multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28", size = 133197 }, { url = "https://files.pythonhosted.org/packages/e4/3a/a13808a7ada62808afccea67837a79d00ad6581440015ef00f726d064c2d/multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972", size = 129754 }, { url = "https://files.pythonhosted.org/packages/77/dd/8540e139eafb240079242da8f8ffdf9d3f4b4ad1aac5a786cd4050923783/multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43", size = 126402 }, { url = "https://files.pythonhosted.org/packages/86/99/e82e1a275d8b1ea16d3a251474262258dbbe41c05cce0c01bceda1fc8ea5/multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada", size = 26421 }, { url = "https://files.pythonhosted.org/packages/86/1c/9fa630272355af7e4446a2c7550c259f11ee422ab2d30ff90a0a71cf3d9e/multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a", size = 28791 }, { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, ] [[package]] name = "mypy" version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } wheels = [ { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 }, { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 }, { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 }, { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 }, { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 }, { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 }, { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 }, { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 }, { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 }, { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 }, { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 }, { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 }, { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 }, { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 }, { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 }, { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 }, { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 }, { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 }, { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 }, { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 }, { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906 }, { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657 }, { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394 }, { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591 }, { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690 }, { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] [[package]] name = "myst-parser" version = "3.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "jinja2" }, { name = "markdown-it-py" }, { name = "mdit-py-plugins" }, { name = "pyyaml" }, { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392 } wheels = [ { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163 }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] [[package]] name = "oauthlib" version = "3.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] [[package]] name = "platformdirs" version = "4.3.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } wheels = [ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, ] [[package]] name = "pre-commit" version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } wheels = [ { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, ] [[package]] name = "propcache" version = "0.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a9/4d/5e5a60b78dbc1d464f8a7bbaeb30957257afdc8512cbb9dfd5659304f5cd/propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", size = 40951 } wheels = [ { url = "https://files.pythonhosted.org/packages/3a/08/1963dfb932b8d74d5b09098507b37e9b96c835ba89ab8aad35aa330f4ff3/propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58", size = 80712 }, { url = "https://files.pythonhosted.org/packages/e6/59/49072aba9bf8a8ed958e576182d46f038e595b17ff7408bc7e8807e721e1/propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b", size = 46301 }, { url = "https://files.pythonhosted.org/packages/33/a2/6b1978c2e0d80a678e2c483f45e5443c15fe5d32c483902e92a073314ef1/propcache-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110", size = 45581 }, { url = "https://files.pythonhosted.org/packages/43/95/55acc9adff8f997c7572f23d41993042290dfb29e404cdadb07039a4386f/propcache-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2", size = 208659 }, { url = "https://files.pythonhosted.org/packages/bd/2c/ef7371ff715e6cd19ea03fdd5637ecefbaa0752fee5b0f2fe8ea8407ee01/propcache-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a", size = 222613 }, { url = "https://files.pythonhosted.org/packages/5e/1c/fef251f79fd4971a413fa4b1ae369ee07727b4cc2c71e2d90dfcde664fbb/propcache-0.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577", size = 221067 }, { url = "https://files.pythonhosted.org/packages/8d/e7/22e76ae6fc5a1708bdce92bdb49de5ebe89a173db87e4ef597d6bbe9145a/propcache-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850", size = 208920 }, { url = "https://files.pythonhosted.org/packages/04/3e/f10aa562781bcd8a1e0b37683a23bef32bdbe501d9cc7e76969becaac30d/propcache-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61", size = 200050 }, { url = "https://files.pythonhosted.org/packages/d0/98/8ac69f638358c5f2a0043809c917802f96f86026e86726b65006830f3dc6/propcache-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37", size = 202346 }, { url = "https://files.pythonhosted.org/packages/ee/78/4acfc5544a5075d8e660af4d4e468d60c418bba93203d1363848444511ad/propcache-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48", size = 199750 }, { url = "https://files.pythonhosted.org/packages/a2/8f/90ada38448ca2e9cf25adc2fe05d08358bda1b9446f54a606ea38f41798b/propcache-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630", size = 201279 }, { url = "https://files.pythonhosted.org/packages/08/31/0e299f650f73903da851f50f576ef09bfffc8e1519e6a2f1e5ed2d19c591/propcache-0.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394", size = 211035 }, { url = "https://files.pythonhosted.org/packages/85/3e/e356cc6b09064bff1c06d0b2413593e7c925726f0139bc7acef8a21e87a8/propcache-0.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b", size = 215565 }, { url = "https://files.pythonhosted.org/packages/8b/54/4ef7236cd657e53098bd05aa59cbc3cbf7018fba37b40eaed112c3921e51/propcache-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336", size = 207604 }, { url = "https://files.pythonhosted.org/packages/1f/27/d01d7799c068443ee64002f0655d82fb067496897bf74b632e28ee6a32cf/propcache-0.2.0-cp310-cp310-win32.whl", hash = "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad", size = 40526 }, { url = "https://files.pythonhosted.org/packages/bb/44/6c2add5eeafb7f31ff0d25fbc005d930bea040a1364cf0f5768750ddf4d1/propcache-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99", size = 44958 }, { url = "https://files.pythonhosted.org/packages/e0/1c/71eec730e12aec6511e702ad0cd73c2872eccb7cad39de8ba3ba9de693ef/propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", size = 80811 }, { url = "https://files.pythonhosted.org/packages/89/c3/7e94009f9a4934c48a371632197406a8860b9f08e3f7f7d922ab69e57a41/propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", size = 46365 }, { url = "https://files.pythonhosted.org/packages/c0/1d/c700d16d1d6903aeab28372fe9999762f074b80b96a0ccc953175b858743/propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", size = 45602 }, { url = "https://files.pythonhosted.org/packages/2e/5e/4a3e96380805bf742712e39a4534689f4cddf5fa2d3a93f22e9fd8001b23/propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", size = 236161 }, { url = "https://files.pythonhosted.org/packages/a5/85/90132481183d1436dff6e29f4fa81b891afb6cb89a7306f32ac500a25932/propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", size = 244938 }, { url = "https://files.pythonhosted.org/packages/4a/89/c893533cb45c79c970834274e2d0f6d64383ec740be631b6a0a1d2b4ddc0/propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", size = 243576 }, { url = "https://files.pythonhosted.org/packages/8c/56/98c2054c8526331a05f205bf45cbb2cda4e58e56df70e76d6a509e5d6ec6/propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", size = 236011 }, { url = "https://files.pythonhosted.org/packages/2d/0c/8b8b9f8a6e1abd869c0fa79b907228e7abb966919047d294ef5df0d136cf/propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504", size = 224834 }, { url = "https://files.pythonhosted.org/packages/18/bb/397d05a7298b7711b90e13108db697732325cafdcd8484c894885c1bf109/propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", size = 224946 }, { url = "https://files.pythonhosted.org/packages/25/19/4fc08dac19297ac58135c03770b42377be211622fd0147f015f78d47cd31/propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", size = 217280 }, { url = "https://files.pythonhosted.org/packages/7e/76/c79276a43df2096ce2aba07ce47576832b1174c0c480fe6b04bd70120e59/propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", size = 220088 }, { url = "https://files.pythonhosted.org/packages/c3/9a/8a8cf428a91b1336b883f09c8b884e1734c87f724d74b917129a24fe2093/propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", size = 233008 }, { url = "https://files.pythonhosted.org/packages/25/7b/768a8969abd447d5f0f3333df85c6a5d94982a1bc9a89c53c154bf7a8b11/propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", size = 237719 }, { url = "https://files.pythonhosted.org/packages/ed/0d/e5d68ccc7976ef8b57d80613ac07bbaf0614d43f4750cf953f0168ef114f/propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", size = 227729 }, { url = "https://files.pythonhosted.org/packages/05/64/17eb2796e2d1c3d0c431dc5f40078d7282f4645af0bb4da9097fbb628c6c/propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", size = 40473 }, { url = "https://files.pythonhosted.org/packages/83/c5/e89fc428ccdc897ade08cd7605f174c69390147526627a7650fb883e0cd0/propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", size = 44921 }, { url = "https://files.pythonhosted.org/packages/7c/46/a41ca1097769fc548fc9216ec4c1471b772cc39720eb47ed7e38ef0006a9/propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", size = 80800 }, { url = "https://files.pythonhosted.org/packages/75/4f/93df46aab9cc473498ff56be39b5f6ee1e33529223d7a4d8c0a6101a9ba2/propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", size = 46443 }, { url = "https://files.pythonhosted.org/packages/0b/17/308acc6aee65d0f9a8375e36c4807ac6605d1f38074b1581bd4042b9fb37/propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", size = 45676 }, { url = "https://files.pythonhosted.org/packages/65/44/626599d2854d6c1d4530b9a05e7ff2ee22b790358334b475ed7c89f7d625/propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", size = 246191 }, { url = "https://files.pythonhosted.org/packages/f2/df/5d996d7cb18df076debae7d76ac3da085c0575a9f2be6b1f707fe227b54c/propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", size = 251791 }, { url = "https://files.pythonhosted.org/packages/2e/6d/9f91e5dde8b1f662f6dd4dff36098ed22a1ef4e08e1316f05f4758f1576c/propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", size = 253434 }, { url = "https://files.pythonhosted.org/packages/3c/e9/1b54b7e26f50b3e0497cd13d3483d781d284452c2c50dd2a615a92a087a3/propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", size = 248150 }, { url = "https://files.pythonhosted.org/packages/a7/ef/a35bf191c8038fe3ce9a414b907371c81d102384eda5dbafe6f4dce0cf9b/propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", size = 233568 }, { url = "https://files.pythonhosted.org/packages/97/d9/d00bb9277a9165a5e6d60f2142cd1a38a750045c9c12e47ae087f686d781/propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", size = 229874 }, { url = "https://files.pythonhosted.org/packages/8e/78/c123cf22469bdc4b18efb78893e69c70a8b16de88e6160b69ca6bdd88b5d/propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", size = 225857 }, { url = "https://files.pythonhosted.org/packages/31/1b/fd6b2f1f36d028820d35475be78859d8c89c8f091ad30e377ac49fd66359/propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", size = 227604 }, { url = "https://files.pythonhosted.org/packages/99/36/b07be976edf77a07233ba712e53262937625af02154353171716894a86a6/propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", size = 238430 }, { url = "https://files.pythonhosted.org/packages/0d/64/5822f496c9010e3966e934a011ac08cac8734561842bc7c1f65586e0683c/propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", size = 244814 }, { url = "https://files.pythonhosted.org/packages/fd/bd/8657918a35d50b18a9e4d78a5df7b6c82a637a311ab20851eef4326305c1/propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", size = 235922 }, { url = "https://files.pythonhosted.org/packages/a8/6f/ec0095e1647b4727db945213a9f395b1103c442ef65e54c62e92a72a3f75/propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", size = 40177 }, { url = "https://files.pythonhosted.org/packages/20/a2/bd0896fdc4f4c1db46d9bc361c8c79a9bf08ccc08ba054a98e38e7ba1557/propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", size = 44446 }, { url = "https://files.pythonhosted.org/packages/a8/a7/5f37b69197d4f558bfef5b4bceaff7c43cc9b51adf5bd75e9081d7ea80e4/propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", size = 78120 }, { url = "https://files.pythonhosted.org/packages/c8/cd/48ab2b30a6b353ecb95a244915f85756d74f815862eb2ecc7a518d565b48/propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", size = 45127 }, { url = "https://files.pythonhosted.org/packages/a5/ba/0a1ef94a3412aab057bd996ed5f0ac7458be5bf469e85c70fa9ceb43290b/propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", size = 44419 }, { url = "https://files.pythonhosted.org/packages/b4/6c/ca70bee4f22fa99eacd04f4d2f1699be9d13538ccf22b3169a61c60a27fa/propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", size = 229611 }, { url = "https://files.pythonhosted.org/packages/19/70/47b872a263e8511ca33718d96a10c17d3c853aefadeb86dc26e8421184b9/propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", size = 234005 }, { url = "https://files.pythonhosted.org/packages/4f/be/3b0ab8c84a22e4a3224719099c1229ddfdd8a6a1558cf75cb55ee1e35c25/propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", size = 237270 }, { url = "https://files.pythonhosted.org/packages/04/d8/f071bb000d4b8f851d312c3c75701e586b3f643fe14a2e3409b1b9ab3936/propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", size = 231877 }, { url = "https://files.pythonhosted.org/packages/93/e7/57a035a1359e542bbb0a7df95aad6b9871ebee6dce2840cb157a415bd1f3/propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", size = 217848 }, { url = "https://files.pythonhosted.org/packages/f0/93/d1dea40f112ec183398fb6c42fde340edd7bab202411c4aa1a8289f461b6/propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", size = 216987 }, { url = "https://files.pythonhosted.org/packages/62/4c/877340871251145d3522c2b5d25c16a1690ad655fbab7bb9ece6b117e39f/propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", size = 212451 }, { url = "https://files.pythonhosted.org/packages/7c/bb/a91b72efeeb42906ef58ccf0cdb87947b54d7475fee3c93425d732f16a61/propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", size = 212879 }, { url = "https://files.pythonhosted.org/packages/9b/7f/ee7fea8faac57b3ec5d91ff47470c6c5d40d7f15d0b1fccac806348fa59e/propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", size = 222288 }, { url = "https://files.pythonhosted.org/packages/ff/d7/acd67901c43d2e6b20a7a973d9d5fd543c6e277af29b1eb0e1f7bd7ca7d2/propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", size = 228257 }, { url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 }, { url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 }, { url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 }, { url = "https://files.pythonhosted.org/packages/38/05/797e6738c9f44ab5039e3ff329540c934eabbe8ad7e63c305c75844bc86f/propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6", size = 81903 }, { url = "https://files.pythonhosted.org/packages/9f/84/8d5edb9a73e1a56b24dd8f2adb6aac223109ff0e8002313d52e5518258ba/propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638", size = 46960 }, { url = "https://files.pythonhosted.org/packages/e7/77/388697bedda984af0d12d68e536b98129b167282da3401965c8450de510e/propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957", size = 46133 }, { url = "https://files.pythonhosted.org/packages/e2/dc/60d444610bc5b1d7a758534f58362b1bcee736a785473f8a39c91f05aad1/propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1", size = 211105 }, { url = "https://files.pythonhosted.org/packages/bc/c6/40eb0dd1de6f8e84f454615ab61f68eb4a58f9d63d6f6eaf04300ac0cc17/propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562", size = 226613 }, { url = "https://files.pythonhosted.org/packages/de/b6/e078b5e9de58e20db12135eb6a206b4b43cb26c6b62ee0fe36ac40763a64/propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d", size = 225587 }, { url = "https://files.pythonhosted.org/packages/ce/4e/97059dd24494d1c93d1efb98bb24825e1930265b41858dd59c15cb37a975/propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12", size = 211826 }, { url = "https://files.pythonhosted.org/packages/fc/23/4dbf726602a989d2280fe130a9b9dd71faa8d3bb8cd23d3261ff3c23f692/propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8", size = 203140 }, { url = "https://files.pythonhosted.org/packages/5b/ce/f3bff82c885dbd9ae9e43f134d5b02516c3daa52d46f7a50e4f52ef9121f/propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8", size = 208841 }, { url = "https://files.pythonhosted.org/packages/29/d7/19a4d3b4c7e95d08f216da97035d0b103d0c90411c6f739d47088d2da1f0/propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb", size = 203315 }, { url = "https://files.pythonhosted.org/packages/db/87/5748212a18beb8d4ab46315c55ade8960d1e2cdc190764985b2d229dd3f4/propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea", size = 204724 }, { url = "https://files.pythonhosted.org/packages/84/2a/c3d2f989fc571a5bad0fabcd970669ccb08c8f9b07b037ecddbdab16a040/propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6", size = 215514 }, { url = "https://files.pythonhosted.org/packages/c9/1f/4c44c133b08bc5f776afcb8f0833889c2636b8a83e07ea1d9096c1e401b0/propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d", size = 220063 }, { url = "https://files.pythonhosted.org/packages/2e/25/280d0a3bdaee68db74c0acd9a472e59e64b516735b59cffd3a326ff9058a/propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798", size = 211620 }, { url = "https://files.pythonhosted.org/packages/28/8c/266898981b7883c1563c35954f9ce9ced06019fdcc487a9520150c48dc91/propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9", size = 41049 }, { url = "https://files.pythonhosted.org/packages/af/53/a3e5b937f58e757a940716b88105ec4c211c42790c1ea17052b46dc16f16/propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df", size = 45587 }, { url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 }, ] [[package]] name = "protobuf" version = "5.28.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/74/6e/e69eb906fddcb38f8530a12f4b410699972ab7ced4e21524ece9d546ac27/protobuf-5.28.3.tar.gz", hash = "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b", size = 422479 } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/c5/05163fad52d7c43e124a545f1372d18266db36036377ad29de4271134a6a/protobuf-5.28.3-cp310-abi3-win32.whl", hash = "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24", size = 419624 }, { url = "https://files.pythonhosted.org/packages/9c/4c/4563ebe001ff30dca9d7ed12e471fa098d9759712980cde1fd03a3a44fb7/protobuf-5.28.3-cp310-abi3-win_amd64.whl", hash = "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868", size = 431464 }, { url = "https://files.pythonhosted.org/packages/1c/f2/baf397f3dd1d3e4af7e3f5a0382b868d25ac068eefe1ebde05132333436c/protobuf-5.28.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687", size = 414743 }, { url = "https://files.pythonhosted.org/packages/85/50/cd61a358ba1601f40e7d38bcfba22e053f40ef2c50d55b55926aecc8fec7/protobuf-5.28.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584", size = 316511 }, { url = "https://files.pythonhosted.org/packages/5d/ae/3257b09328c0b4e59535e497b0c7537d4954038bdd53a2f0d2f49d15a7c4/protobuf-5.28.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135", size = 316624 }, { url = "https://files.pythonhosted.org/packages/57/b5/ee3d918f536168def73b3f49edeba065429ab3a7e7b033d33e69c46ddff9/protobuf-5.28.3-cp39-cp39-win32.whl", hash = "sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535", size = 419648 }, { url = "https://files.pythonhosted.org/packages/53/54/e1bdf6f1d29828ddb6aca0a83bf208ab1d5f88126f34e17e487b2cd20d93/protobuf-5.28.3-cp39-cp39-win_amd64.whl", hash = "sha256:70585a70fc2dd4818c51287ceef5bdba6387f88a578c86d47bb34669b5552c36", size = 431591 }, { url = "https://files.pythonhosted.org/packages/ad/c3/2377c159e28ea89a91cf1ca223f827ae8deccb2c9c401e5ca233cd73002f/protobuf-5.28.3-py3-none-any.whl", hash = "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed", size = 169511 }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] [[package]] name = "pygments" version = "2.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, ] [[package]] name = "pytest" version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, ] [[package]] name = "pytest-asyncio" version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855 } wheels = [ { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024 }, ] [[package]] name = "pytest-cov" version = "6.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } wheels = [ { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, ] [[package]] name = "pytest-freezer" version = "0.4.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "freezegun" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/fa/a93d40dd50f712c276a5a15f9c075bee932cc4d28c376e60b4a35904976d/pytest_freezer-0.4.8.tar.gz", hash = "sha256:8ee2f724b3ff3540523fa355958a22e6f4c1c819928b78a7a183ae4248ce6ee6", size = 3212 } wheels = [ { url = "https://files.pythonhosted.org/packages/d8/4e/ba488639516a341810aeaeb4b32b70abb0923e53f7c4d14d673dc114d35a/pytest_freezer-0.4.8-py3-none-any.whl", hash = "sha256:644ce7ddb8ba52b92a1df0a80a699bad2b93514c55cf92e9f2517b68ebe74814", size = 3228 }, ] [[package]] name = "pytest-mock" version = "3.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } wheels = [ { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, ] [[package]] name = "pytest-socket" version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/05/ff/90c7e1e746baf3d62ce864c479fd53410b534818b9437413903596f81580/pytest_socket-0.7.0.tar.gz", hash = "sha256:71ab048cbbcb085c15a4423b73b619a8b35d6a307f46f78ea46be51b1b7e11b3", size = 12389 } wheels = [ { url = "https://files.pythonhosted.org/packages/19/58/5d14cb5cb59409e491ebe816c47bf81423cd03098ea92281336320ae5681/pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45", size = 6754 }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] [[package]] name = "pytz" version = "2024.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } wheels = [ { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 }, { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 }, { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 }, { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 }, { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 }, { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 }, { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 }, { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 }, { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 }, ] [[package]] name = "requests" version = "2.32.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] [[package]] name = "requests-mock" version = "1.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901 } wheels = [ { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695 }, ] [[package]] name = "ring-doorbell" version = "0.9.13" source = { editable = "." } dependencies = [ { name = "aiofiles" }, { name = "aiohttp" }, { name = "async-timeout" }, { name = "asyncclick" }, { name = "firebase-messaging" }, { name = "oauthlib" }, { name = "pytz" }, { name = "typing-extensions" }, { name = "websockets" }, ] [package.optional-dependencies] docs = [ { name = "myst-parser" }, { name = "sphinx" }, { name = "sphinx-rtd-theme" }, ] [package.dev-dependencies] dev = [ { name = "aioresponses" }, { name = "mock" }, { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-freezer" }, { name = "pytest-mock" }, { name = "pytest-socket" }, { name = "requests-mock" }, { name = "ruff" }, { name = "types-aiofiles" }, { name = "types-oauthlib" }, { name = "types-pytz" }, ] [package.metadata] requires-dist = [ { name = "aiofiles", specifier = ">=23" }, { name = "aiohttp", specifier = ">=3" }, { name = "async-timeout", specifier = ">=3.0.0" }, { name = "asyncclick", specifier = ">=8.1.7.1" }, { name = "firebase-messaging", specifier = ">=0.4.0" }, { name = "myst-parser", marker = "extra == 'docs'" }, { name = "oauthlib", specifier = ">=3.0.0,<4" }, { name = "pytz", specifier = ">=2022.0" }, { name = "sphinx", marker = "extra == 'docs'", specifier = "<7.2.6" }, { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = "~=1.3" }, { name = "typing-extensions", specifier = ">=4.12.2,<5.0" }, { name = "websockets", specifier = ">=13.0.0" }, ] [package.metadata.requires-dev] dev = [ { name = "aioresponses", specifier = "~=0.7" }, { name = "mock" }, { name = "mypy", specifier = "~=1.0" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-freezer", specifier = "~=0.4" }, { name = "pytest-mock" }, { name = "pytest-socket" }, { name = "requests-mock" }, { name = "ruff" }, { name = "types-aiofiles", specifier = ">=23" }, { name = "types-oauthlib", specifier = ">=3.0.0,<4" }, { name = "types-pytz", specifier = ">=2022.0" }, ] [[package]] name = "ruff" version = "0.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/d6/a2373f3ba7180ddb44420d2a9d1f1510e1a4d162b3d27282bedcb09c8da9/ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44", size = 3276537 } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/77/e889ee3ce7fd8baa3ed1b77a03b9fb8ec1be68be1418261522fd6a5405e0/ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea", size = 10518283 }, { url = "https://files.pythonhosted.org/packages/da/c8/0a47de01edf19fb22f5f9b7964f46a68d0bdff20144d134556ffd1ba9154/ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b", size = 10317691 }, { url = "https://files.pythonhosted.org/packages/41/17/9885e4a0eeae07abd2a4ebabc3246f556719f24efa477ba2739146c4635a/ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a", size = 9940999 }, { url = "https://files.pythonhosted.org/packages/3e/cd/46b6f7043597eb318b5f5482c8ae8f5491cccce771e85f59d23106f2d179/ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99", size = 10772437 }, { url = "https://files.pythonhosted.org/packages/5d/87/afc95aeb8bc78b1d8a3461717a4419c05aa8aa943d4c9cbd441630f85584/ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c", size = 10299156 }, { url = "https://files.pythonhosted.org/packages/65/fa/04c647bb809c4d65e8eae1ed1c654d9481b21dd942e743cd33511687b9f9/ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9", size = 11325819 }, { url = "https://files.pythonhosted.org/packages/90/26/7dad6e7d833d391a8a1afe4ee70ca6f36c4a297d3cca83ef10e83e9aacf3/ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362", size = 12023927 }, { url = "https://files.pythonhosted.org/packages/24/a0/be5296dda6428ba8a13bda8d09fbc0e14c810b485478733886e61597ae2b/ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df", size = 11589702 }, { url = "https://files.pythonhosted.org/packages/26/3f/7602eb11d2886db545834182a9dbe500b8211fcbc9b4064bf9d358bbbbb4/ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3", size = 12782936 }, { url = "https://files.pythonhosted.org/packages/4c/5d/083181bdec4ec92a431c1291d3fff65eef3ded630a4b55eb735000ef5f3b/ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c", size = 11138488 }, { url = "https://files.pythonhosted.org/packages/b7/23/c12cdef58413cee2436d6a177aa06f7a366ebbca916cf10820706f632459/ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2", size = 10744474 }, { url = "https://files.pythonhosted.org/packages/29/61/a12f3b81520083cd7c5caa24ba61bb99fd1060256482eff0ef04cc5ccd1b/ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70", size = 10369029 }, { url = "https://files.pythonhosted.org/packages/08/2a/c013f4f3e4a54596c369cee74c24870ed1d534f31a35504908b1fc97017a/ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd", size = 10867481 }, { url = "https://files.pythonhosted.org/packages/d5/f7/685b1e1d42a3e94ceb25eab23c70bdd8c0ab66a43121ef83fe6db5a58756/ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426", size = 11237117 }, { url = "https://files.pythonhosted.org/packages/03/20/401132c0908e8837625e3b7e32df9962e7cd681a4df1e16a10e2a5b4ecda/ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468", size = 8783511 }, { url = "https://files.pythonhosted.org/packages/1d/5c/4d800fca7854f62ad77f2c0d99b4b585f03e2d87a6ec1ecea85543a14a3c/ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f", size = 9559876 }, { url = "https://files.pythonhosted.org/packages/5b/bc/cc8a6a5ca4960b226dc15dd8fb511dd11f2014ff89d325c0b9b9faa9871f/ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6", size = 8939733 }, ] [[package]] name = "six" version = "1.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } wheels = [ { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] name = "snowballstemmer" version = "2.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, ] [[package]] name = "sphinx" version = "7.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alabaster" }, { name = "babel" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "docutils" }, { name = "imagesize" }, { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "packaging" }, { name = "pygments" }, { name = "requests" }, { name = "snowballstemmer" }, { name = "sphinxcontrib-applehelp" }, { name = "sphinxcontrib-devhelp" }, { name = "sphinxcontrib-htmlhelp" }, { name = "sphinxcontrib-jsmath" }, { name = "sphinxcontrib-qthelp" }, { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/aa/7f284cb72eafff634eab41ddf18a9bcf2f335d830fa3879072b877c1d72b/sphinx-7.2.5.tar.gz", hash = "sha256:1a9290001b75c497fd087e92b0334f1bbfa1a1ae7fddc084990c4b7bd1130b88", size = 7014676 } wheels = [ { url = "https://files.pythonhosted.org/packages/a6/54/f4fcf7113eb051a46476ecce9485c463f58dbc3887c06dbfe1e67a8ce7c0/sphinx-7.2.5-py3-none-any.whl", hash = "sha256:9269f9ed2821c9ebd30e4204f5c2339f5d4980e377bc89cb2cb6f9b17409c20a", size = 3207795 }, ] [[package]] name = "sphinx-rtd-theme" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "sphinx" }, { name = "sphinxcontrib-jquery" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/3e/477c5b3ed78b6818d673f63512db12ace8c89e83eb9eecc913f9e2cc8416/sphinx_rtd_theme-1.3.0.tar.gz", hash = "sha256:590b030c7abb9cf038ec053b95e5380b5c70d61591eb0b552063fbe7c41f0931", size = 2785069 } wheels = [ { url = "https://files.pythonhosted.org/packages/18/01/76f40a18e9209bb098c1c1313c823dbbd001b23a2db71e7fd4eb5a48559c/sphinx_rtd_theme-1.3.0-py2.py3-none-any.whl", hash = "sha256:46ddef89cc2416a81ecfbeaceab1881948c014b1b6e4450b815311a89fb977b0", size = 2824803 }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } wheels = [ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, ] [[package]] name = "sphinxcontrib-jquery" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331 } wheels = [ { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104 }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } wheels = [ { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, ] [[package]] name = "tomli" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1e/e4/1b6cbcc82d8832dd0ce34767d5c560df8a3547ad8cbc427f34601415930a/tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8", size = 16622 } wheels = [ { url = "https://files.pythonhosted.org/packages/de/f7/4da0ffe1892122c9ea096c57f64c2753ae5dd3ce85488802d11b0992cc6d/tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391", size = 13750 }, ] [[package]] name = "types-aiofiles" version = "24.1.0.20240626" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/13/e9/013940b017c313c2e15c64017268fdb0c25e0638621fb8a5d9ebe00fb0f4/types-aiofiles-24.1.0.20240626.tar.gz", hash = "sha256:48604663e24bc2d5038eac05ccc33e75799b0779e93e13d6a8f711ddc306ac08", size = 9357 } wheels = [ { url = "https://files.pythonhosted.org/packages/c3/ad/c4b3275d21c5be79487c4f6ed7cd13336997746fe099236cb29256a44a90/types_aiofiles-24.1.0.20240626-py3-none-any.whl", hash = "sha256:7939eca4a8b4f9c6491b6e8ef160caee9a21d32e18534a57d5ed90aee47c66b4", size = 9389 }, ] [[package]] name = "types-oauthlib" version = "3.2.0.20240806" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4f/aa/73047cdeb1a0cdbca65d163742570cff58ebd93d0847604469bc08809bf4/types-oauthlib-3.2.0.20240806.tar.gz", hash = "sha256:31a8d7f7bffc067a4143d30167694ccb304316aced04de50741014155f502043", size = 16483 } wheels = [ { url = "https://files.pythonhosted.org/packages/12/fc/15f3e0611429455b60734e4b2b566440d47ea284b5897b9b3f2638c2bd6a/types_oauthlib-3.2.0.20240806-py3-none-any.whl", hash = "sha256:581bb8e194700d16ae1f0b62a6039261ed1afd0b88e78782e1c48f6507c52f34", size = 34735 }, ] [[package]] name = "types-pytz" version = "2024.2.0.20241003" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/66/d0/73aa3063a9ef9881bd7103cb4ae379bfd8fafda0e86b01b694d676313a4b/types-pytz-2024.2.0.20241003.tar.gz", hash = "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44", size = 5474 } wheels = [ { url = "https://files.pythonhosted.org/packages/86/60/2a2977ce0f91255bbb668350b127a801a06ad37c326a2e5bfd52f03e0784/types_pytz-2024.2.0.20241003-py3-none-any.whl", hash = "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7", size = 5245 }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } wheels = [ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] [[package]] name = "urllib3" version = "2.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } wheels = [ { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, ] [[package]] name = "virtualenv" version = "20.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/75/53316a5a8050069228a2f6d11f32046cfa94fbb6cc3f08703f59b873de2e/virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa", size = 7650368 } wheels = [ { url = "https://files.pythonhosted.org/packages/10/f9/0919cf6f1432a8c4baa62511f8f8da8225432d22e83e3476f5be1a1edc6e/virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0", size = 4276702 }, ] [[package]] name = "websockets" version = "14.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } wheels = [ { url = "https://files.pythonhosted.org/packages/af/91/b1b375dbd856fd5fff3f117de0e520542343ecaf4e8fc60f1ac1e9f5822c/websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29", size = 161950 }, { url = "https://files.pythonhosted.org/packages/61/8f/4d52f272d3ebcd35e1325c646e98936099a348374d4a6b83b524bded8116/websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179", size = 159601 }, { url = "https://files.pythonhosted.org/packages/c4/b1/29e87b53eb1937992cdee094a0988aadc94f25cf0b37e90c75eed7123d75/websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250", size = 159854 }, { url = "https://files.pythonhosted.org/packages/3f/e6/752a2f5e8321ae2a613062676c08ff2fccfb37dc837a2ee919178a372e8a/websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0", size = 168835 }, { url = "https://files.pythonhosted.org/packages/60/27/ca62de7877596926321b99071639275e94bb2401397130b7cf33dbf2106a/websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0", size = 167844 }, { url = "https://files.pythonhosted.org/packages/7e/db/f556a1d06635c680ef376be626c632e3f2bbdb1a0189d1d1bffb061c3b70/websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199", size = 168157 }, { url = "https://files.pythonhosted.org/packages/b3/bc/99e5f511838c365ac6ecae19674eb5e94201aa4235bd1af3e6fa92c12905/websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58", size = 168561 }, { url = "https://files.pythonhosted.org/packages/c6/e7/251491585bad61c79e525ac60927d96e4e17b18447cc9c3cfab47b2eb1b8/websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078", size = 167979 }, { url = "https://files.pythonhosted.org/packages/ac/98/7ac2e4eeada19bdbc7a3a66a58e3ebdf33648b9e1c5b3f08c3224df168cf/websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434", size = 167925 }, { url = "https://files.pythonhosted.org/packages/ab/3d/09e65c47ee2396b7482968068f6e9b516221e1032b12dcf843b9412a5dfb/websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10", size = 162831 }, { url = "https://files.pythonhosted.org/packages/8a/67/59828a3d09740e6a485acccfbb66600632f2178b6ed1b61388ee96f17d5a/websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e", size = 163266 }, { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, { url = "https://files.pythonhosted.org/packages/4d/23/ac9d8c5ec7b90efc3687d60474ef7e698f8b75cb7c9dfedad72701e797c9/websockets-14.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a", size = 161945 }, { url = "https://files.pythonhosted.org/packages/c5/6b/ffa450e3b736a86ae6b40ce20a758ac9af80c96a18548f6c323ed60329c5/websockets-14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6", size = 159600 }, { url = "https://files.pythonhosted.org/packages/74/62/f90d1fd57ea7337ecaa99f17c31a544b9dcdb7c7c32a3d3997ccc42d57d3/websockets-14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56", size = 159850 }, { url = "https://files.pythonhosted.org/packages/35/dd/1e71865de1f3c265e11d02b0b4c76178f84351c6611e515fbe3d2bd1b98c/websockets-14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c", size = 168616 }, { url = "https://files.pythonhosted.org/packages/ba/ae/0d069b52e26d48402dbe90c7581eb6a5bed5d7dbe3d9ca3cf1033859d58e/websockets-14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b", size = 167619 }, { url = "https://files.pythonhosted.org/packages/1c/3f/d3f2df62704c53e0296f0ce714921b6a15df10e2e463734c737b1d9e2522/websockets-14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78", size = 167921 }, { url = "https://files.pythonhosted.org/packages/e0/e2/2dcb295bdae9393070cea58c790d87d1d36149bb4319b1da6014c8a36d42/websockets-14.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735", size = 168343 }, { url = "https://files.pythonhosted.org/packages/6b/fd/fa48e8b4e10e2c165cbfc16dada7405b4008818be490fc6b99a4928e232a/websockets-14.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a", size = 167745 }, { url = "https://files.pythonhosted.org/packages/42/45/79db33f2b744d2014b40946428e6c37ce944fde8791d82e1c2f4d4a67d96/websockets-14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc", size = 167705 }, { url = "https://files.pythonhosted.org/packages/da/27/f66507db34ca9c79562f28fa5983433f7b9080fd471cc188906006d36ba4/websockets-14.1-cp39-cp39-win32.whl", hash = "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4", size = 162828 }, { url = "https://files.pythonhosted.org/packages/11/25/bb8f81a4ec94f595adb845608c5ec9549cb6b446945b292fe61807c7c95b/websockets-14.1-cp39-cp39-win_amd64.whl", hash = "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979", size = 163271 }, { url = "https://files.pythonhosted.org/packages/fb/cd/382a05a1ba2a93bd9fb807716a660751295df72e77204fb130a102fcdd36/websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8", size = 159633 }, { url = "https://files.pythonhosted.org/packages/b7/a0/fa7c62e2952ef028b422fbf420f9353d9dd4dfaa425de3deae36e98c0784/websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e", size = 159867 }, { url = "https://files.pythonhosted.org/packages/c1/94/954b4924f868db31d5f0935893c7a8446515ee4b36bb8ad75a929469e453/websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098", size = 161121 }, { url = "https://files.pythonhosted.org/packages/7a/2e/f12bbb41a8f2abb76428ba4fdcd9e67b5b364a3e7fa97c88f4d6950aa2d4/websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb", size = 160731 }, { url = "https://files.pythonhosted.org/packages/13/97/b76979401f2373af1fe3e08f960b265cecab112e7dac803446fb98351a52/websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7", size = 160681 }, { url = "https://files.pythonhosted.org/packages/39/9c/16916d9a436c109a1d7ba78817e8fee357b78968be3f6e6f517f43afa43d/websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d", size = 163316 }, { url = "https://files.pythonhosted.org/packages/0f/57/50fd09848a80a1b63a572c610f230f8a17590ca47daf256eb28a0851df73/websockets-14.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370", size = 159633 }, { url = "https://files.pythonhosted.org/packages/d7/2f/db728b0c7962ad6a13ced8286325bf430b59722d943e7f6bdbd8a78e2bfe/websockets-14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a", size = 159863 }, { url = "https://files.pythonhosted.org/packages/fa/e4/21e7481936fbfffee138edb488a6184eb3468b402a8181b95b9e44f6a676/websockets-14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7", size = 161119 }, { url = "https://files.pythonhosted.org/packages/64/2d/efb6cf716d4f9da60190756e06f8db2066faf1ae4a4a8657ab136dfcc7a8/websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0", size = 160724 }, { url = "https://files.pythonhosted.org/packages/40/b0/a70b972d853c3f26040834fcff3dd45c8a0292af9f5f0b36f9fbb82d5d44/websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1", size = 160676 }, { url = "https://files.pythonhosted.org/packages/4a/76/f9da7f97476cc7b8c74829bb4851f1faf660455839689ffcc354b52860a7/websockets-14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5", size = 163311 }, { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, ] [[package]] name = "yarl" version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/4b/53db4ecad4d54535aff3dfda1f00d6363d79455f62b11b8ca97b82746bd2/yarl-1.18.0.tar.gz", hash = "sha256:20d95535e7d833889982bfe7cc321b7f63bf8879788fee982c76ae2b24cfb715", size = 180098 } wheels = [ { url = "https://files.pythonhosted.org/packages/80/8b/305e1bde6bbf900bb8909a4884488764ee5950dda4da06cec885c06dae68/yarl-1.18.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:074fee89caab89a97e18ef5f29060ef61ba3cae6cd77673acc54bfdd3214b7b7", size = 141186 }, { url = "https://files.pythonhosted.org/packages/6a/85/a15e439d8faa6bd09a536d87ca7a32daa50cf8820cf220edbced702348a0/yarl-1.18.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b026cf2c32daf48d90c0c4e406815c3f8f4cfe0c6dfccb094a9add1ff6a0e41a", size = 94097 }, { url = "https://files.pythonhosted.org/packages/12/9d/7d39082baae943f138df1bb96914f8d53fd65eb131b9d0965917b009b35d/yarl-1.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae38bd86eae3ba3d2ce5636cc9e23c80c9db2e9cb557e40b98153ed102b5a736", size = 91915 }, { url = "https://files.pythonhosted.org/packages/c0/35/7e6fbfeb413f281dda59d4a9fce7a0c43cb1f22cb6ac25151d4c4ce51651/yarl-1.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:685cc37f3f307c6a8e879986c6d85328f4c637f002e219f50e2ef66f7e062c1d", size = 315086 }, { url = "https://files.pythonhosted.org/packages/76/2e/61b854cca176d8952d1448b15d59b9b4df27648e4cc9c1a2a01449238b21/yarl-1.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8254dbfce84ee5d1e81051ee7a0f1536c108ba294c0fdb5933476398df0654f3", size = 330221 }, { url = "https://files.pythonhosted.org/packages/98/66/975c36deeb069888274c2edfa9d6aef44c7574e9b11bb0687130ddd02558/yarl-1.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20de4a8b04de70c49698dc2390b7fd2d18d424d3b876371f9b775e2b462d4b41", size = 326650 }, { url = "https://files.pythonhosted.org/packages/a4/06/511e5ac4e562cbd605a05c90875e36ec5bac93da0dc55c730b4b3b09face/yarl-1.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0a2074a37285570d54b55820687de3d2f2b9ecf1b714e482e48c9e7c0402038", size = 319437 }, { url = "https://files.pythonhosted.org/packages/7c/6a/8f6f8b17b28ed6eaaf20f5a80d391ae1c1bd5437af9ed552b9eb8903b11c/yarl-1.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f576ed278860df2721a5d57da3381040176ef1d07def9688a385c8330db61a1", size = 309966 }, { url = "https://files.pythonhosted.org/packages/b5/54/4d9dcbdaba18a948f8bea5b65835bfcc5a931426c79d8d2dafe45264ece8/yarl-1.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3a3709450a574d61be6ac53d582496014342ea34876af8dc17cc16da32826c9a", size = 319519 }, { url = "https://files.pythonhosted.org/packages/42/b7/de7fcde2c414d33a2be5ac9c31469ad33874a26a5e3421b2a9505a1a10ee/yarl-1.18.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bd80ed29761490c622edde5dd70537ca8c992c2952eb62ed46984f8eff66d6e8", size = 321455 }, { url = "https://files.pythonhosted.org/packages/4e/49/8ed0dc1973876f20b63fe66986f300fd0721f3d644b6a64be12ec436c197/yarl-1.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:32141e13a1d5a48525e519c9197d3f4d9744d818d5c7d6547524cc9eccc8971e", size = 324564 }, { url = "https://files.pythonhosted.org/packages/0c/76/63209f71efde8875670441875ef1a46383a06f578f6babf819b0cf79ebd7/yarl-1.18.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8b8d3e4e014fb4274f1c5bf61511d2199e263909fb0b8bda2a7428b0894e8dc6", size = 336798 }, { url = "https://files.pythonhosted.org/packages/a8/f3/77e0cdee76359dade383b61eb995a3a2efcef3d64da3222f3cf52d38bd38/yarl-1.18.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:701bb4a8f4de191c8c0cc9a1e6d5142f4df880e9d1210e333b829ca9425570ed", size = 337902 }, { url = "https://files.pythonhosted.org/packages/96/d9/0f97875e2498196a9b5561de32f3f25208485c7b43d676a65a2ee6c12fd7/yarl-1.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a45d94075ac0647621eaaf693c8751813a3eccac455d423f473ffed38c8ac5c9", size = 331620 }, { url = "https://files.pythonhosted.org/packages/71/a3/e3bd136838d29fec4acc4919bcfd2bd33296f6c281c829fa277e72bc2590/yarl-1.18.0-cp310-cp310-win32.whl", hash = "sha256:34176bfb082add67cb2a20abd85854165540891147f88b687a5ed0dc225750a0", size = 84045 }, { url = "https://files.pythonhosted.org/packages/fd/20/a474648c2b49c9ed5eb0e7137add6373e5d9220eda7e6d4b43d306e67672/yarl-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:73553bbeea7d6ec88c08ad8027f4e992798f0abc459361bf06641c71972794dc", size = 90221 }, { url = "https://files.pythonhosted.org/packages/06/45/6ad7135d1c4ad3a6a49e2c37dc78a1805a7871879c03c3495d64c9605d49/yarl-1.18.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b8e8c516dc4e1a51d86ac975b0350735007e554c962281c432eaa5822aa9765c", size = 141283 }, { url = "https://files.pythonhosted.org/packages/45/6d/24b70ae33107d6eba303ed0ebfdf1164fe2219656e7594ca58628ebc0f1d/yarl-1.18.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e6b4466714a73f5251d84b471475850954f1fa6acce4d3f404da1d55d644c34", size = 94082 }, { url = "https://files.pythonhosted.org/packages/8a/0e/da720989be11b662ca847ace58f468b52310a9b03e52ac62c144755f9d75/yarl-1.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c893f8c1a6d48b25961e00922724732d00b39de8bb0b451307482dc87bddcd74", size = 92017 }, { url = "https://files.pythonhosted.org/packages/f5/76/e5c91681fa54658943cb88673fb19b3355c3a8ae911a33a2621b6320990d/yarl-1.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13aaf2bdbc8c86ddce48626b15f4987f22e80d898818d735b20bd58f17292ee8", size = 340359 }, { url = "https://files.pythonhosted.org/packages/cf/77/02cf72f09dea20980dea4ebe40dfb2c24916b864aec869a19f715428e0f0/yarl-1.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd21c0128e301851de51bc607b0a6da50e82dc34e9601f4b508d08cc89ee7929", size = 356336 }, { url = "https://files.pythonhosted.org/packages/17/66/83a88d04e4fc243dd26109f3e3d6412f67819ab1142dadbce49706ef4df4/yarl-1.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205de377bd23365cd85562c9c6c33844050a93661640fda38e0567d2826b50df", size = 353730 }, { url = "https://files.pythonhosted.org/packages/76/77/0b205a532d22756ab250ab21924d362f910a23d641c82faec1c4ad7f6077/yarl-1.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed69af4fe2a0949b1ea1d012bf065c77b4c7822bad4737f17807af2adb15a73c", size = 343882 }, { url = "https://files.pythonhosted.org/packages/0b/47/2081ddce3da6096889c3947bdc21907d0fa15939909b10219254fe116841/yarl-1.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e1c18890091aa3cc8a77967943476b729dc2016f4cfe11e45d89b12519d4a93", size = 335873 }, { url = "https://files.pythonhosted.org/packages/25/3c/437304394494e757ae927c9a81bacc4bcdf7351a1d4e811d95b02cb6dbae/yarl-1.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:91b8fb9427e33f83ca2ba9501221ffaac1ecf0407f758c4d2f283c523da185ee", size = 347725 }, { url = "https://files.pythonhosted.org/packages/c6/fb/fa6c642bc052fbe6370ed5da765579650510157dea354fe9e8177c3bc34a/yarl-1.18.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:536a7a8a53b75b2e98ff96edb2dfb91a26b81c4fed82782035767db5a465be46", size = 346161 }, { url = "https://files.pythonhosted.org/packages/b0/09/8c0cf68a0fcfe3b060c9e5857bb35735bc72a4cf4075043632c636d007e9/yarl-1.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a64619a9c47c25582190af38e9eb382279ad42e1f06034f14d794670796016c0", size = 349924 }, { url = "https://files.pythonhosted.org/packages/bf/4b/1efe10fd51e2cedf53195d688fa270efbcd64a015c61d029d49c20bf0af7/yarl-1.18.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c73a6bbc97ba1b5a0c3c992ae93d721c395bdbb120492759b94cc1ac71bc6350", size = 361865 }, { url = "https://files.pythonhosted.org/packages/0b/1b/2b5efd6df06bf938f7e154dee8e2ab22d148f3311a92bf4da642aaaf2fc5/yarl-1.18.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a173401d7821a2a81c7b47d4e7d5c4021375a1441af0c58611c1957445055056", size = 366030 }, { url = "https://files.pythonhosted.org/packages/f8/db/786a5684f79278e62271038a698f56a51960f9e643be5d3eff82712f0b1c/yarl-1.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7520e799b1f84e095cce919bd6c23c9d49472deeef25fe1ef960b04cca51c3fc", size = 358902 }, { url = "https://files.pythonhosted.org/packages/91/2f/437d0de062f1a3e3cb17573971b3832232443241133580c2ba3da5001d06/yarl-1.18.0-cp311-cp311-win32.whl", hash = "sha256:c4cb992d8090d5ae5f7afa6754d7211c578be0c45f54d3d94f7781c495d56716", size = 84138 }, { url = "https://files.pythonhosted.org/packages/9d/85/035719a9266bce85ecde820aa3f8c46f3b18c3d7ba9ff51367b2fa4ae2a2/yarl-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:52c136f348605974c9b1c878addd6b7a60e3bf2245833e370862009b86fa4689", size = 90765 }, { url = "https://files.pythonhosted.org/packages/23/36/c579b80a5c76c0d41c8e08baddb3e6940dfc20569db579a5691392c52afa/yarl-1.18.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1ece25e2251c28bab737bdf0519c88189b3dd9492dc086a1d77336d940c28ced", size = 142376 }, { url = "https://files.pythonhosted.org/packages/0c/5f/e247dc7c0607a0c505fea6c839721844bee55686dfb183c7d7b8ef8a9cb1/yarl-1.18.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:454902dc1830d935c90b5b53c863ba2a98dcde0fbaa31ca2ed1ad33b2a7171c6", size = 94692 }, { url = "https://files.pythonhosted.org/packages/eb/e1/3081b578a6f21961711b9a1c49c2947abb3b0d0dd9537378fb06777ce8ee/yarl-1.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01be8688fc211dc237e628fcc209dda412d35de7642453059a0553747018d075", size = 92527 }, { url = "https://files.pythonhosted.org/packages/2f/fa/d9e1b9fbafa4cc82cd3980b5314741b33c2fe16308d725449a23aed32021/yarl-1.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d26f1fa9fa2167bb238f6f4b20218eb4e88dd3ef21bb8f97439fa6b5313e30d", size = 332096 }, { url = "https://files.pythonhosted.org/packages/93/b6/dd27165114317875838e216214fb86338dc63d2e50855a8f2a12de2a7fe5/yarl-1.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b234a4a9248a9f000b7a5dfe84b8cb6210ee5120ae70eb72a4dcbdb4c528f72f", size = 342047 }, { url = "https://files.pythonhosted.org/packages/fc/9f/bad434b5279ae7a356844e14dc771c3d29eb928140bbc01621af811c8a27/yarl-1.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe94d1de77c4cd8caff1bd5480e22342dbd54c93929f5943495d9c1e8abe9f42", size = 341712 }, { url = "https://files.pythonhosted.org/packages/9a/9f/63864f43d131ba8c8cdf1bde5dd3f02f0eff8a7c883a5d7fad32f204fda5/yarl-1.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4c90c5363c6b0a54188122b61edb919c2cd1119684999d08cd5e538813a28e", size = 336654 }, { url = "https://files.pythonhosted.org/packages/20/30/b4542bbd9be73de155213207eec019f6fe6495885f7dd59aa1ff705a041b/yarl-1.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a98ecadc5a241c9ba06de08127ee4796e1009555efd791bac514207862b43d", size = 325484 }, { url = "https://files.pythonhosted.org/packages/69/bc/e2a9808ec26989cf0d1b98fe7b3cc45c1c6506b5ea4fe43ece5991f28f34/yarl-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9106025c7f261f9f5144f9aa7681d43867eed06349a7cfb297a1bc804de2f0d1", size = 344213 }, { url = "https://files.pythonhosted.org/packages/e2/17/0ee5a68886aca1a8071b0d24a1e1c0fd9970dead2ef2d5e26e027fb7ce88/yarl-1.18.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f275ede6199d0f1ed4ea5d55a7b7573ccd40d97aee7808559e1298fe6efc8dbd", size = 340517 }, { url = "https://files.pythonhosted.org/packages/fd/db/1fe4ef38ee852bff5ec8f5367d718b3a7dac7520f344b8e50306f68a2940/yarl-1.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f7edeb1dcc7f50a2c8e08b9dc13a413903b7817e72273f00878cb70e766bdb3b", size = 346234 }, { url = "https://files.pythonhosted.org/packages/b4/ee/5e5bccdb821eb9949ba66abb4d19e3299eee00282e37b42f65236120e892/yarl-1.18.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c083f6dd6951b86e484ebfc9c3524b49bcaa9c420cb4b2a78ef9f7a512bfcc85", size = 359625 }, { url = "https://files.pythonhosted.org/packages/3f/43/95a64d9e7ab4aa1c34fc5ea0edb35b581bc6ad33fd960a8ae34c2040b319/yarl-1.18.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:80741ec5b471fbdfb997821b2842c59660a1c930ceb42f8a84ba8ca0f25a66aa", size = 364239 }, { url = "https://files.pythonhosted.org/packages/40/19/09ce976c624c9d3cc898f0be5035ddef0c0759d85b2313321cfe77b69915/yarl-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1a3297b9cad594e1ff0c040d2881d7d3a74124a3c73e00c3c71526a1234a9f7", size = 357599 }, { url = "https://files.pythonhosted.org/packages/7d/35/6f33fd29791af2ec161aebe8abe63e788c2b74a6c7e8f29c92e5f5e96849/yarl-1.18.0-cp312-cp312-win32.whl", hash = "sha256:cd6ab7d6776c186f544f893b45ee0c883542b35e8a493db74665d2e594d3ca75", size = 83832 }, { url = "https://files.pythonhosted.org/packages/4e/8e/cdb40ef98597be107de67b11e2f1f23f911e0f1416b938885d17a338e304/yarl-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:039c299a0864d1f43c3e31570045635034ea7021db41bf4842693a72aca8df3a", size = 90132 }, { url = "https://files.pythonhosted.org/packages/2b/77/2196b657c66f97adaef0244e9e015f30eac0df59c31ad540f79ce328feed/yarl-1.18.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6fb64dd45453225f57d82c4764818d7a205ee31ce193e9f0086e493916bd4f72", size = 140512 }, { url = "https://files.pythonhosted.org/packages/0e/d8/2bb6e26fddba5c01bad284e4571178c651b97e8e06318efcaa16e07eb9fd/yarl-1.18.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3adaaf9c6b1b4fc258584f4443f24d775a2086aee82d1387e48a8b4f3d6aecf6", size = 93875 }, { url = "https://files.pythonhosted.org/packages/54/e4/99fbb884dd9f814fb0037dc1783766bb9edcd57b32a76f3ec5ac5c5772d7/yarl-1.18.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:da206d1ec78438a563c5429ab808a2b23ad7bc025c8adbf08540dde202be37d5", size = 91705 }, { url = "https://files.pythonhosted.org/packages/3b/a2/5bd86eca9449e6b15d3b08005cf4e58e3da972240c2bee427b358c311549/yarl-1.18.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:576d258b21c1db4c6449b1c572c75d03f16a482eb380be8003682bdbe7db2f28", size = 333325 }, { url = "https://files.pythonhosted.org/packages/94/50/a218da5f159cd985685bc72c500bb1a7fd2d60035d2339b8a9d9e1f99194/yarl-1.18.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e547c0a375c4bfcdd60eef82e7e0e8698bf84c239d715f5c1278a73050393", size = 344121 }, { url = "https://files.pythonhosted.org/packages/a4/e3/830ae465811198b4b5ebecd674b5b3dca4d222af2155eb2144bfe190bbb8/yarl-1.18.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3818eabaefb90adeb5e0f62f047310079d426387991106d4fbf3519eec7d90a", size = 345163 }, { url = "https://files.pythonhosted.org/packages/7a/74/05c4326877ca541eee77b1ef74b7ac8081343d3957af8f9291ca6eca6fec/yarl-1.18.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f72421246c21af6a92fbc8c13b6d4c5427dfd949049b937c3b731f2f9076bd", size = 339130 }, { url = "https://files.pythonhosted.org/packages/29/42/842f35aa1dae25d132119ee92185e8c75d8b9b7c83346506bd31e9fa217f/yarl-1.18.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fa7d37f2ada0f42e0723632993ed422f2a679af0e200874d9d861720a54f53e", size = 326418 }, { url = "https://files.pythonhosted.org/packages/f9/ed/65c0514f2d1e8b92a61f564c914381d078766cab38b5fbde355b3b3af1fb/yarl-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:42ba84e2ac26a3f252715f8ec17e6fdc0cbf95b9617c5367579fafcd7fba50eb", size = 345204 }, { url = "https://files.pythonhosted.org/packages/23/31/351f64f0530c372fa01160f38330f44478e7bf3092f5ce2bfcb91605561d/yarl-1.18.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6a49ad0102c0f0ba839628d0bf45973c86ce7b590cdedf7540d5b1833ddc6f00", size = 341652 }, { url = "https://files.pythonhosted.org/packages/49/aa/0c6e666c218d567727c1d040d01575685e7f9b18052fd68a59c9f61fe5d9/yarl-1.18.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96404e8d5e1bbe36bdaa84ef89dc36f0e75939e060ca5cd45451aba01db02902", size = 347257 }, { url = "https://files.pythonhosted.org/packages/36/0b/33a093b0e13bb8cd0f27301779661ff325270b6644929001f8f33307357d/yarl-1.18.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a0509475d714df8f6d498935b3f307cd122c4ca76f7d426c7e1bb791bcd87eda", size = 359735 }, { url = "https://files.pythonhosted.org/packages/a8/92/dcc0b37c48632e71ffc2b5f8b0509347a0bde55ab5862ff755dce9dd56c4/yarl-1.18.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1ff116f0285b5c8b3b9a2680aeca29a858b3b9e0402fc79fd850b32c2bcb9f8b", size = 365982 }, { url = "https://files.pythonhosted.org/packages/0e/39/30e2a24a7a6c628dccb13eb6c4a03db5f6cd1eb2c6cda56a61ddef764c11/yarl-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2580c1d7e66e6d29d6e11855e3b1c6381971e0edd9a5066e6c14d79bc8967af", size = 360128 }, { url = "https://files.pythonhosted.org/packages/76/13/12b65dca23b1fb8ae44269a4d24048fd32ac90b445c985b0a46fdfa30cfe/yarl-1.18.0-cp313-cp313-win32.whl", hash = "sha256:14408cc4d34e202caba7b5ac9cc84700e3421a9e2d1b157d744d101b061a4a88", size = 309888 }, { url = "https://files.pythonhosted.org/packages/f6/60/478d3d41a4bf0b9e7dca74d870d114e775d1ff7156b7d1e0e9972e8f97fd/yarl-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1db1537e9cb846eb0ff206eac667f627794be8b71368c1ab3207ec7b6f8c5afc", size = 315459 }, { url = "https://files.pythonhosted.org/packages/20/b2/75bfeacf949045f0455a56c397183a89e01cd51183569208745c63e0265e/yarl-1.18.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fa2c9cb607e0f660d48c54a63de7a9b36fef62f6b8bd50ff592ce1137e73ac7d", size = 142514 }, { url = "https://files.pythonhosted.org/packages/a5/7e/002c031e12ca3b833fd0612a76425219ecfc5c73ca58de3c61e2a7c95dd2/yarl-1.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c0f4808644baf0a434a3442df5e0bedf8d05208f0719cedcd499e168b23bfdc4", size = 94718 }, { url = "https://files.pythonhosted.org/packages/ff/6c/f41813685c220b11d9fc8a26a5c8fd10b44c52420785a14f948b69518281/yarl-1.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7db9584235895a1dffca17e1c634b13870852094f6389b68dcc6338086aa7b08", size = 92528 }, { url = "https://files.pythonhosted.org/packages/f0/93/ed88fc9102ea31ab3bd20131eb305f81abc4e715adc00c8c2e4882dc056a/yarl-1.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:309f8d27d6f93ceeeb80aa6980e883aa57895270f7f41842b92247e65d7aeddf", size = 317072 }, { url = "https://files.pythonhosted.org/packages/27/09/acd37201a513a1b060a503400692837ee5169699ba02c728baedaa837050/yarl-1.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:609ffd44fed2ed88d9b4ef62ee860cf86446cf066333ad4ce4123505b819e581", size = 336745 }, { url = "https://files.pythonhosted.org/packages/f0/9a/a22435685010deed686759c4207b920bb92f3ed71f5d7cf49c673e065106/yarl-1.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f172b8b2c72a13a06ea49225a9c47079549036ad1b34afa12d5491b881f5b993", size = 331424 }, { url = "https://files.pythonhosted.org/packages/2c/8b/a9f89dbb9322a5c7be24bc11b06dab031932808b69f8be65f0ab149b7a59/yarl-1.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d89ae7de94631b60d468412c18290d358a9d805182373d804ec839978b120422", size = 321233 }, { url = "https://files.pythonhosted.org/packages/29/48/7d258d42354d1220a6f2a357e0734ed183024e931c38b1e884631b4ab15c/yarl-1.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:466d31fd043ef9af822ee3f1df8fdff4e8c199a7f4012c2642006af240eade17", size = 313300 }, { url = "https://files.pythonhosted.org/packages/3c/44/a664651d965a4a1a71bce53a255107204e420aa3a8c8260566065d12b155/yarl-1.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7609b8462351c4836b3edce4201acb6dd46187b207c589b30a87ffd1813b48dc", size = 324760 }, { url = "https://files.pythonhosted.org/packages/32/91/7ed533566f62b38bda1b2454b7799fe7e4e62bc1e10f6c96489d16ee3f35/yarl-1.18.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d9d4f5e471e8dc49b593a80766c2328257e405f943c56a3dc985c125732bc4cf", size = 322419 }, { url = "https://files.pythonhosted.org/packages/11/f9/a3dc2ab42f73717d97d0fd37be63660d8faf3ddaa024704218d4956f5118/yarl-1.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:67b336c15e564d76869c9a21316f90edf546809a5796a083b8f57c845056bc01", size = 332551 }, { url = "https://files.pythonhosted.org/packages/c8/d5/a99321e2214614639fdc69ae144936888f62d9c0c781224f2d0ecb1c66dd/yarl-1.18.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b212452b80cae26cb767aa045b051740e464c5129b7bd739c58fbb7deb339e7b", size = 339705 }, { url = "https://files.pythonhosted.org/packages/07/28/a4a3ed46dcf8c48cdb8747194d0c6905fb59a1af2f90fea18bcc341ba034/yarl-1.18.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:38b39b7b3e692b6c92b986b00137a3891eddb66311b229d1940dcbd4f025083c", size = 340745 }, { url = "https://files.pythonhosted.org/packages/2f/06/c12452cee751829e7371f70bfb7b68a1fdd9dc6783c39a9ca8fa21ce033a/yarl-1.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7ee6884a8848792d58b854946b685521f41d8871afa65e0d4a774954e9c9e89", size = 336355 }, { url = "https://files.pythonhosted.org/packages/eb/2e/13a2a7b9a33c760588e413b2d13e206a0e75e5d518dd2f95bd78a90d4a95/yarl-1.18.0-cp39-cp39-win32.whl", hash = "sha256:b4095c5019bb889aa866bf12ed4c85c0daea5aafcb7c20d1519f02a1e738f07f", size = 84564 }, { url = "https://files.pythonhosted.org/packages/d6/a2/699d2cc6594a00ee3bfc8d4983ece8fb2c225cb32452088ce728c24617a2/yarl-1.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:2d90f2e4d16a5b0915ee065218b435d2ef619dd228973b1b47d262a6f7cd8fa5", size = 90703 }, { url = "https://files.pythonhosted.org/packages/30/9c/3f7ab894a37b1520291247cbc9ea6756228d098dae5b37eec848d404a204/yarl-1.18.0-py3-none-any.whl", hash = "sha256:dbf53db46f7cf176ee01d8d98c39381440776fcda13779d269a8ba664f69bec0", size = 44840 }, ] [[package]] name = "zipp" version = "3.21.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, ]