pax_global_header 0000666 0000000 0000000 00000000064 14721352732 0014520 g ustar 00root root 0000000 0000000 52 comment=8aaba5962f5529834321fd14338d53bb3241bdd2
python-ring-doorbell-0.9.13/ 0000775 0000000 0000000 00000000000 14721352732 0015670 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/.github/ 0000775 0000000 0000000 00000000000 14721352732 0017230 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/.github/actions/ 0000775 0000000 0000000 00000000000 14721352732 0020670 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/.github/actions/setup/ 0000775 0000000 0000000 00000000000 14721352732 0022030 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/.github/actions/setup/action.yml 0000664 0000000 0000000 00000002650 14721352732 0024033 0 ustar 00root root 0000000 0000000 ---
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.yml 0000664 0000000 0000000 00000000054 14721352732 0023017 0 ustar 00root root 0000000 0000000 template: |
## What's Changed
$CHANGES
python-ring-doorbell-0.9.13/.github/workflows/ 0000775 0000000 0000000 00000000000 14721352732 0021265 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/.github/workflows/ci.yml 0000664 0000000 0000000 00000005160 14721352732 0022405 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000654 14721352732 0024560 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000007210 14721352732 0023456 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000005207 14721352732 0023124 0 ustar 00root root 0000000 0000000 name: 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_generator 0000664 0000000 0000000 00000001034 14721352732 0023226 0 ustar 00root root 0000000 0000000 output=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/.gitignore 0000664 0000000 0000000 00000001473 14721352732 0017665 0 ustar 00root root 0000000 0000000 # ---> 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.yml 0000664 0000000 0000000 00000000031 14721352732 0017600 0 ustar 00root root 0000000 0000000 python:
enabled: false
python-ring-doorbell-0.9.13/.pre-commit-config.yaml 0000664 0000000 0000000 00000002165 14721352732 0022155 0 ustar 00root root 0000000 0000000 repos:
- 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.yaml 0000664 0000000 0000000 00000002175 14721352732 0021124 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000122174 14721352732 0017510 0 ustar 00root root 0000000 0000000 # 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.rst 0000664 0000000 0000000 00000007221 14721352732 0020333 0 ustar 00root root 0000000 0000000 ============
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/LICENSE 0000664 0000000 0000000 00000016744 14721352732 0016711 0 ustar 00root root 0000000 0000000 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.rst 0000664 0000000 0000000 00000023403 14721352732 0017361 0 ustar 00root root 0000000 0000000 =====================
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.md 0000664 0000000 0000000 00000007311 14721352732 0017525 0 ustar 00root root 0000000 0000000 # 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.yml 0000664 0000000 0000000 00000000034 14721352732 0020014 0 ustar 00root root 0000000 0000000 theme: jekyll-theme-minimal
python-ring-doorbell-0.9.13/docs/ 0000775 0000000 0000000 00000000000 14721352732 0016620 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/docs/Makefile 0000664 0000000 0000000 00000001176 14721352732 0020265 0 ustar 00root root 0000000 0000000 # 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.bat 0000664 0000000 0000000 00000001444 14721352732 0020230 0 ustar 00root root 0000000 0000000 @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.toml 0000664 0000000 0000000 00000000213 14721352732 0020453 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14721352732 0020120 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/docs/source/changelog.md 0000664 0000000 0000000 00000000040 14721352732 0022363 0 ustar 00root root 0000000 0000000 :::{include} ../../CHANGELOG.md
python-ring-doorbell-0.9.13/docs/source/conf.py 0000664 0000000 0000000 00000002500 14721352732 0021414 0 ustar 00root root 0000000 0000000 # 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.rst 0000664 0000000 0000000 00000000044 14721352732 0023357 0 ustar 00root root 0000000 0000000 .. include:: ../../CONTRIBUTING.rst
python-ring-doorbell-0.9.13/docs/source/index.rst 0000664 0000000 0000000 00000000720 14721352732 0021760 0 ustar 00root root 0000000 0000000 .. 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.toml 0000664 0000000 0000000 00000007525 14721352732 0020615 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 14721352732 0020511 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/ring_doorbell/__init__.py 0000664 0000000 0000000 00000002252 14721352732 0022623 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000022150 14721352732 0022024 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000006606 14721352732 0022160 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000067065 14721352732 0021650 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000022612 14721352732 0022214 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000046140 14721352732 0022540 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000002013 14721352732 0022200 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000000567 14721352732 0023254 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000020233 14721352732 0022477 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000010656 14721352732 0022227 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14721352732 0022007 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/ring_doorbell/listen/__init__.py 0000664 0000000 0000000 00000000515 14721352732 0024121 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000027503 14721352732 0025257 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000001052 14721352732 0025372 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000023401 14721352732 0022204 0 ustar 00root root 0000000 0000000 # 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.typed 0000664 0000000 0000000 00000000000 14721352732 0022176 0 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/ring_doorbell/ring.py 0000664 0000000 0000000 00000037772 14721352732 0022042 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000015174 14721352732 0023375 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000012102 14721352732 0022034 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000033732 14721352732 0023575 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000002577 14721352732 0017234 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000002344 14721352732 0020260 0 ustar 00root root 0000000 0000000 """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/ 0000775 0000000 0000000 00000000000 14721352732 0017032 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/tests/__init__.py 0000664 0000000 0000000 00000000053 14721352732 0021141 0 ustar 00root root 0000000 0000000 """Tests for Ring Door Bell components."""
python-ring-doorbell-0.9.13/tests/conftest.py 0000664 0000000 0000000 00000025577 14721352732 0021251 0 ustar 00root root 0000000 0000000 """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/ 0000775 0000000 0000000 00000000000 14721352732 0020703 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/tests/fixtures/listen/ 0000775 0000000 0000000 00000000000 14721352732 0022201 5 ustar 00root root 0000000 0000000 python-ring-doorbell-0.9.13/tests/fixtures/listen/camera_motion_analytics.json 0000664 0000000 0000000 00000000425 14721352732 0027761 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000000242 14721352732 0030734 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000001511 14721352732 0026700 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000001713 14721352732 0027373 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000000111 14721352732 0026536 0 ustar 00root root 0000000 0000000 {
"snapshot_uuid": "12345678-ab12-1234-5678-123456abcdef:12345678"
}
python-ring-doorbell-0.9.13/tests/fixtures/listen/doorbot_ding_gcmdata.json 0000664 0000000 0000000 00000001473 14721352732 0027232 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000000251 14721352732 0025077 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000000502 14721352732 0025077 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000000423 14721352732 0027763 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000000146 14721352732 0030743 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000003613 14721352732 0026711 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000000457 14721352732 0027755 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000001065 14721352732 0026446 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000040464 14721352732 0024247 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000024344 14721352732 0025754 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000004622 14721352732 0025075 0 ustar 00root root 0000000 0000000 [
{
"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.json 0000664 0000000 0000000 00000001065 14721352732 0027210 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000001065 14721352732 0030372 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000001230 14721352732 0026042 0 ustar 00root root 0000000 0000000 [{
"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.json 0000664 0000000 0000000 00000001347 14721352732 0025460 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000001422 14721352732 0024133 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000005147 14721352732 0026225 0 ustar 00root root 0000000 0000000 [
{
"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.json 0000664 0000000 0000000 00000024552 14721352732 0026365 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000001225 14721352732 0025656 0 ustar 00root root 0000000 0000000 [
{
"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.json 0000664 0000000 0000000 00000004417 14721352732 0026476 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000000271 14721352732 0023735 0 ustar 00root root 0000000 0000000 {
"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.json 0000664 0000000 0000000 00000002340 14721352732 0024277 0 ustar 00root root 0000000 0000000 {
"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.toml 0000664 0000000 0000000 00000001221 14721352732 0020665 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000030240 14721352732 0021211 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000015316 14721352732 0021747 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000010721 14721352732 0021565 0 ustar 00root root 0000000 0000000 """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.py 0000664 0000000 0000000 00000027044 14721352732 0021411 0 ustar 00root root 0000000 0000000 """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.lock 0000664 0000000 0000000 00001064751 14721352732 0017212 0 ustar 00root root 0000000 0000000 version = 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 },
]